Skip to content

Commit fa28299

Browse files
authored
Merge pull request kevoreilly#3070 from wmetcalf/fix/oidc-claims-nested-extra-data
allauth: read OIDC claims from nested userinfo in extra_data
2 parents d8d35bc + ae1f3b7 commit fa28299

1 file changed

Lines changed: 29 additions & 2 deletions

File tree

web/web/allauth_adapters.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,30 @@ def get_package(cls):
193193

194194
# ── Helpers ───────────────────────────────────────────────────────────────────
195195

196+
def _claims(extra: dict) -> dict:
197+
"""Flatten allauth's OIDC extra_data to the actual claim dict.
198+
199+
The openid_connect provider stores extra_data as
200+
``{"id_token": <jwt>, "userinfo": {...claims...}}`` — the OIDC claims
201+
(email, groups, preferred_username, sub) live under ``userinfo``, not at the
202+
top level. Other providers store the claims flat, so fall back to the dict
203+
itself when there's no nested ``userinfo``.
204+
205+
Gated on ``"id_token"`` (which the openid_connect provider always puts at the
206+
top level) so the function is idempotent: calling it on already-flattened
207+
claims — or on a flat provider's data — returns them unchanged even if they
208+
happen to carry their own ``userinfo`` key.
209+
"""
210+
if isinstance(extra, dict) and "id_token" in extra:
211+
ui = extra.get("userinfo")
212+
if isinstance(ui, dict) and ui:
213+
return ui
214+
return extra or {}
215+
216+
196217
def _extract_groups(extra: dict) -> set:
197218
"""Return the set of IdP group names from token extra data."""
219+
extra = _claims(extra)
198220
oidc_cfg = getattr(settings, "OIDC_CFG", None) or {}
199221
claim = oidc_cfg.get("groups_claim") or "groups"
200222
raw = extra.get(claim) or []
@@ -233,6 +255,7 @@ def _apply_idp_roles_and_email(user, extra: dict) -> bool:
233255
but empty claim is honoured (the user really is in no groups → demote).
234256
"""
235257
changed = False
258+
extra = _claims(extra)
236259

237260
email = extra.get("email") or ""
238261
if email and user.email != email:
@@ -303,7 +326,11 @@ def pre_social_login(self, request, sociallogin):
303326
Raises ImmediateHttpResponse — caught by allauth's complete_login
304327
wrapper and rendered as a user-facing error page (a bare
305328
ValidationError here would bubble up as a 500)."""
306-
user_email = sociallogin.account.extra_data.get("email") or ""
329+
user_email = (
330+
_claims(sociallogin.account.extra_data or {}).get("email")
331+
or (sociallogin.user.email if sociallogin.user else "")
332+
or ""
333+
)
307334
if settings.SOCIAL_AUTH_EMAIL_DOMAIN:
308335
if not user_email:
309336
# Fail closed: a domain allowlist is configured but the IdP sent
@@ -359,7 +386,7 @@ def save_user(self, request, sociallogin, form=None):
359386
if no subject is present — is appended to guarantee uniqueness.
360387
"""
361388
user = super().save_user(request, sociallogin, form)
362-
extra = sociallogin.account.extra_data or {}
389+
extra = _claims(sociallogin.account.extra_data or {})
363390

364391
# ── username (provisioning only — kept stable across later logins) ──
365392
identifier = (

0 commit comments

Comments
 (0)