77from django .core .exceptions import SuspiciousOperation
88from django .utils .translation import gettext_lazy as _
99
10- import requests
11- from mozilla_django_oidc .auth import (
12- OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend ,
10+ from lasuite .oidc_login .backends import (
11+ OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend ,
1312)
1413
1514from core .enums import MailboxRoleChoices
2524logger = logging .getLogger (__name__ )
2625
2726
28- class OIDCAuthenticationBackend (MozillaOIDCAuthenticationBackend ):
27+ class OIDCAuthenticationBackend (LaSuiteOIDCAuthenticationBackend ):
2928 """Custom OpenID Connect (OIDC) Authentication Backend.
3029
3130 This class overrides the default OIDC Authentication Backend to accommodate differences
3231 in the User and Identity models, and handles signed and/or encrypted UserInfo response.
3332 """
3433
35- def get_userinfo (self , access_token , id_token , payload ):
36- """Return user details dictionary.
37-
38- Parameters:
39- - access_token (str): The access token.
40- - id_token (str): The id token (unused).
41- - payload (dict): The token payload (unused).
42-
43- Note: The id_token and payload parameters are unused in this implementation,
44- but were kept to preserve base method signature.
34+ def get_or_create_user (self , access_token , id_token , payload ):
35+ """
36+ Return a User based on userinfo. Create a new user if no match is found.
4537
46- Note: It handles signed and/or encrypted UserInfo Response. It is required by
47- Agent Connect, which follows the OIDC standard. It forces us to override the
48- base method, which deal with 'application/json' response.
38+ Args:
39+ access_token (str): The access token.
40+ id_token (str): The ID token.
41+ payload (dict): The user payload.
4942
5043 Returns:
51- - dict: User details dictionary obtained from the OpenID Connect user endpoint.
52- """
44+ User: An existing or newly created User instance.
5345
54- user_response = requests .get (
55- self .OIDC_OP_USER_ENDPOINT ,
56- headers = {"Authorization" : f"Bearer { access_token } " },
57- verify = self .get_settings ("OIDC_VERIFY_SSL" , True ),
58- timeout = self .get_settings ("OIDC_TIMEOUT" , None ),
59- proxies = self .get_settings ("OIDC_PROXY" , None ),
60- )
61- user_response .raise_for_status ()
46+ Raises:
47+ Exception: Raised when user creation is not allowed and no existing user is found.
6248
63- try :
64- userinfo = user_response .json ()
65- except ValueError :
66- try :
67- userinfo = self .verify_token (user_response .text )
68- except Exception as e :
69- raise SuspiciousOperation (
70- _ ("Invalid response format or token verification failed" )
71- ) from e
72-
73- return userinfo
74-
75- def verify_claims (self , claims ):
7649 """
77- Verify the presence of essential claims and the "sub" (which is mandatory as defined
78- by the OIDC specification) to decide if authentication should be allowed.
79- """
80- essential_claims = settings .USER_OIDC_ESSENTIAL_CLAIMS
81- missing_claims = [claim for claim in essential_claims if claim not in claims ]
82-
83- if missing_claims :
84- logger .error ("Missing essential claims: %s" , missing_claims )
85- return False
86-
87- return True
88-
89- def get_or_create_user (self , access_token , id_token , payload ):
90- """Return a User based on userinfo. Create a new user if no match is found."""
91-
50+ _user_created = False
9251 user_info = self .get_userinfo (access_token , id_token , payload )
9352
9453 if not self .verify_claims (user_info ):
95- raise SuspiciousOperation ("Claims verification failed." )
54+ msg = "Claims verification failed"
55+ raise SuspiciousOperation (msg )
9656
9757 sub = user_info ["sub" ]
98- email = user_info . get ( "email" )
99-
100- # Get user's full name from OIDC fields defined in settings
101- full_name = self . compute_full_name ( user_info )
58+ if not sub :
59+ raise SuspiciousOperation (
60+ "User info contained no recognizable user identification"
61+ )
10262
103- claims = { "email" : email , "full_name" : full_name }
63+ email = user_info . get ( "email" )
10464
105- try :
106- user = User .objects .get_user_by_sub_or_email (sub , email )
107- except DuplicateEmailError as err :
108- raise SuspiciousOperation (err .message ) from err
65+ claims = {
66+ self .OIDC_USER_SUB_FIELD : sub ,
67+ "email" : email ,
68+ }
69+ claims .update (** self .get_extra_claims (user_info ))
10970
71+ # if sub is absent, try matching on email
72+ user = self .get_existing_user (sub , email )
11073 self .create_testdomain ()
11174
11275 if user :
@@ -115,30 +78,29 @@ def get_or_create_user(self, access_token, id_token, payload):
11578 self .update_user_if_needed (user , claims )
11679
11780 elif self .should_create_user (email ):
118- user = User .objects .create (sub = sub , password = "!" , ** claims ) # noqa: S106
81+ user = self .create_user (claims )
82+ _user_created = True
11983
84+ self .post_get_or_create_user (user , claims , _user_created )
85+ return user
86+
87+ def post_get_or_create_user (self , user , claims , _user_created ):
88+ """Post-get or create user."""
12089 if user :
12190 self .autojoin_mailbox (user )
122- return user
123-
124- return None
12591
126- def compute_full_name (self , user_info ):
127- """Compute user's full name based on OIDC fields in settings."""
128- name_fields = settings .USER_OIDC_FIELDS_TO_FULLNAME
129- full_name = " " .join (
130- user_info [field ] for field in name_fields if user_info .get (field )
131- )
132- return full_name or None
92+ def get_extra_claims (self , user_info ):
93+ """Get extra claims."""
94+ return {
95+ "full_name" : self .compute_full_name (user_info ),
96+ }
13397
134- def update_user_if_needed (self , user , claims ):
135- """Update user claims if they have changed."""
136- has_changed = any (
137- value and value != getattr (user , key ) for key , value in claims .items ()
138- )
139- if has_changed :
140- updated_claims = {key : value for key , value in claims .items () if value }
141- self .UserModel .objects .filter (id = user .id ).update (** updated_claims )
98+ def get_existing_user (self , sub , email ):
99+ """Get an existing user by sub or email."""
100+ try :
101+ return User .objects .get_user_by_sub_or_email (sub , email )
102+ except DuplicateEmailError as err :
103+ raise SuspiciousOperation (err .message ) from err
142104
143105 def create_testdomain (self ):
144106 """Create the test domain if it doesn't exist."""
0 commit comments