Skip to content

Commit 3084671

Browse files
committed
Avoid log-in loops caused by ungranted scopes
This was found when using scope(s) not supported by the IdP
1 parent 6ed6737 commit 3084671

File tree

2 files changed

+23
-5
lines changed

2 files changed

+23
-5
lines changed

identity/django.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def wrapper(request, *args, **kwargs):
339339
user = auth.get_user()
340340
if user:
341341
if scopes:
342-
result = auth.get_token_for_user(scopes)
342+
result = auth.get_token_for_user(scopes) # Silently via RT
343343
if isinstance(result, dict) and "access_token" in result:
344344
context = dict(
345345
user=user,
@@ -349,10 +349,14 @@ def wrapper(request, *args, **kwargs):
349349
expires_in=result.get("expires_in", 300),
350350
refresh_token=result.get("refresh_token"),
351351
)
352-
if result.get("scope"):
353-
context["scopes"] = result["scope"].split()
352+
context["scopes"] = result["scope"].split() if result.get(
353+
"scope") else scopes
354354
else:
355-
pass # Token request failed. So we set no context.
355+
logger.error(
356+
"Access token unavailable. Error: %s, Desc: %s, keys: %s",
357+
result.get("error"), result.get("error_description"),
358+
result.keys())
359+
context = None # Token request failed
356360
else:
357361
context = {"user": user}
358362
if context:

identity/web.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Auth(object):
1414
_TOKEN_CACHE = "_token_cache"
1515
_AUTH_FLOW = "_auth_flow"
1616
_USER = "_logged_in_user"
17+
_EXPLICITLY_REQUESTED_SCOPES = f"{__name__}.explicitly_requested_scopes"
1718
__STATE_NO_OP = f"{__name__}.no_op" # A special state to indicate an auth response shall be ignored
1819
__NEXT_LINK = f"{__name__}.next_link" # The next page after a successful auth
1920
def __init__(
@@ -124,6 +125,7 @@ def log_in(
124125
flow = app.initiate_device_flow(_scopes)
125126
if "error" in flow:
126127
return flow
128+
flow[self._EXPLICITLY_REQUESTED_SCOPES] = _scopes # Can be different than the flow["scope"] which is possibly injected by OIDC library
127129
flow[self.__NEXT_LINK] = next_link
128130
self._session[self._AUTH_FLOW] = flow
129131
if redirect_uri:
@@ -185,6 +187,18 @@ def complete_log_in(self, auth_response=None):
185187
)
186188
if "error" in result:
187189
return result
190+
if "scope" in result:
191+
# Only partial scopes were granted, others were likely unsupported.
192+
# according to https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
193+
ungranted_scopes = set(
194+
auth_flow[self._EXPLICITLY_REQUESTED_SCOPES]
195+
) - set(result["scope"].split())
196+
if ungranted_scopes:
197+
return {
198+
"error": "invalid_scope", # https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
199+
"error_description": "Ungranted scope(s): {}".format(
200+
' '.join(ungranted_scopes)),
201+
}
188202
# TODO: Reject a re-log-in with a different account?
189203
self._save_user_into_session(result["id_token_claims"])
190204
self._save_cache(cache)
@@ -211,7 +225,7 @@ def get_user(self):
211225
return self._load_user_from_session()
212226

213227
def get_token_for_user(self, scopes):
214-
"""Get access token for the current user, with specified scopes.
228+
"""Get access token silently for the current user, with specified scopes.
215229
216230
:param list scopes:
217231
A list of scopes that your app will need to use.

0 commit comments

Comments
 (0)