Skip to content

Commit 3e2a502

Browse files
committed
♻️(authentication) use django-lasuite oidc backend
Insatll django-lasuite and our custom authentication backend to inherit from the LaSuiteOIDCAuthenticationBackend
1 parent afcced7 commit 3e2a502

File tree

10 files changed

+293
-696
lines changed

10 files changed

+293
-696
lines changed

docs/env.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ The application uses a new environment file structure with `.defaults` and `.loc
154154
| `OIDC_OP_TOKEN_ENDPOINT` | `http://keycloak:8000/realms/messages/protocol/openid-connect/token` | OIDC token endpoint | Required |
155155
| `OIDC_OP_USER_ENDPOINT` | `http://keycloak:8000/realms/messages/protocol/openid-connect/userinfo` | OIDC user info endpoint | Required |
156156
| `OIDC_OP_LOGOUT_ENDPOINT` | None | OIDC logout endpoint | Optional |
157+
| `OIDC_USERINFO_ESSENTIAL_CLAIMS` | `[]` | Essential OIDC claims | Optional |
158+
| `OIDC_USERINFO_FULLNAME_FIELDS` | `["first_name", "last_name"]` | Fields to use for full name | Optional |
159+
157160

158161
### OIDC Advanced Settings
159162

@@ -176,14 +179,6 @@ The application uses a new environment file structure with `.defaults` and `.loc
176179
| `LOGOUT_REDIRECT_URL` | `http://localhost:8900` | Post-logout redirect URL | Optional |
177180
| `ALLOW_LOGOUT_GET_METHOD` | `True` | Allow GET method for logout | Optional |
178181

179-
### User Mapping
180-
181-
| Variable | Default | Description | Required |
182-
|----------|---------|-------------|----------|
183-
| `USER_OIDC_ESSENTIAL_CLAIMS` | `[]` | Essential OIDC claims | Optional |
184-
| `USER_OIDC_FIELDS_TO_FULLNAME` | `["first_name", "last_name"]` | Fields for full name | Optional |
185-
| `USER_OIDC_FIELD_TO_SHORTNAME` | `first_name` | Field for short name | Optional |
186-
187182
## Security & CORS
188183

189184
| Variable | Default | Description | Required |

src/backend/core/authentication/backends.py

Lines changed: 46 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from django.core.exceptions import SuspiciousOperation
88
from 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

1514
from core.enums import MailboxRoleChoices
@@ -25,88 +24,52 @@
2524
logger = 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."""
Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
11
"""Authentication URLs for the People core app."""
22

3-
from django.urls import path
4-
5-
from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls
6-
7-
from .views import OIDCLogoutCallbackView, OIDCLogoutView
3+
from django.urls import include, path
84

95
urlpatterns = [
10-
# Override the default 'logout/' path from Mozilla Django OIDC with our custom view.
11-
path("logout/", OIDCLogoutView.as_view(), name="oidc_logout_custom"),
12-
path(
13-
"logout-callback/",
14-
OIDCLogoutCallbackView.as_view(),
15-
name="oidc_logout_callback",
16-
),
17-
*mozzila_oidc_urls,
6+
path("", include("lasuite.oidc_login.urls")),
187
]

src/backend/core/authentication/views.py

Lines changed: 0 additions & 137 deletions
This file was deleted.

0 commit comments

Comments
 (0)