Skip to content

Commit e0a8700

Browse files
authored
Merge pull request #389 from AzureAD/max_age
Add max_age support
2 parents 0deba2e + dac99e0 commit e0a8700

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

msal/application.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ def initiate_auth_code_flow(
558558
login_hint=None, # type: Optional[str]
559559
domain_hint=None, # type: Optional[str]
560560
claims_challenge=None,
561+
max_age=None,
561562
):
562563
"""Initiate an auth code flow.
563564
@@ -588,6 +589,17 @@ def initiate_auth_code_flow(
588589
`here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
589590
`here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
590591
592+
:param int max_age:
593+
OPTIONAL. Maximum Authentication Age.
594+
Specifies the allowable elapsed time in seconds
595+
since the last time the End-User was actively authenticated.
596+
If the elapsed time is greater than this value,
597+
Microsoft identity platform will actively re-authenticate the End-User.
598+
599+
MSAL Python will also automatically validate the auth_time in ID token.
600+
601+
New in version 1.15.
602+
591603
:return:
592604
The auth code flow. It is a dict in this form::
593605
@@ -617,6 +629,7 @@ def initiate_auth_code_flow(
617629
domain_hint=domain_hint,
618630
claims=_merge_claims_challenge_and_capabilities(
619631
self._client_capabilities, claims_challenge),
632+
max_age=max_age,
620633
)
621634
flow["claims_challenge"] = claims_challenge
622635
return flow
@@ -1403,6 +1416,7 @@ def acquire_token_interactive(
14031416
timeout=None,
14041417
port=None,
14051418
extra_scopes_to_consent=None,
1419+
max_age=None,
14061420
**kwargs):
14071421
"""Acquire token interactively i.e. via a local browser.
14081422
@@ -1448,6 +1462,17 @@ def acquire_token_interactive(
14481462
in the same interaction, but for which you won't get back a
14491463
token for in this particular operation.
14501464
1465+
:param int max_age:
1466+
OPTIONAL. Maximum Authentication Age.
1467+
Specifies the allowable elapsed time in seconds
1468+
since the last time the End-User was actively authenticated.
1469+
If the elapsed time is greater than this value,
1470+
Microsoft identity platform will actively re-authenticate the End-User.
1471+
1472+
MSAL Python will also automatically validate the auth_time in ID token.
1473+
1474+
New in version 1.15.
1475+
14511476
:return:
14521477
- A dict containing no "error" key,
14531478
and typically contains an "access_token" key.
@@ -1466,6 +1491,7 @@ def acquire_token_interactive(
14661491
port=port or 0),
14671492
prompt=prompt,
14681493
login_hint=login_hint,
1494+
max_age=max_age,
14691495
timeout=timeout,
14701496
auth_params={
14711497
"claims": claims,

msal/oauth2cli/oidc.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None)
4242
"""
4343
decoded = json.loads(decode_part(id_token.split('.')[1]))
4444
err = None # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
45-
_now = now or time.time()
45+
_now = int(now or time.time())
4646
skew = 120 # 2 minutes
4747
if _now + skew < decoded.get("nbf", _now - 1): # nbf is optional per JWT specs
4848
# This is not an ID token validation, but a JWT validation
@@ -67,14 +67,14 @@ def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None)
6767
# the Client and the Token Endpoint (which it is during _obtain_token()),
6868
# the TLS server validation MAY be used to validate the issuer
6969
# in place of checking the token signature.
70-
if _now > decoded["exp"]:
70+
if _now - skew > decoded["exp"]:
7171
err = "9. The current time MUST be before the time represented by the exp Claim."
7272
if nonce and nonce != decoded.get("nonce"):
7373
err = ("11. Nonce must be the same value "
7474
"as the one that was sent in the Authentication Request.")
7575
if err:
76-
raise RuntimeError("%s The id_token was: %s" % (
77-
err, json.dumps(decoded, indent=2)))
76+
raise RuntimeError("%s Current epoch = %s. The id_token was: %s" % (
77+
err, _now, json.dumps(decoded, indent=2)))
7878
return decoded
7979

8080

@@ -187,6 +187,8 @@ def initiate_auth_code_flow(
187187
flow = super(Client, self).initiate_auth_code_flow(
188188
scope=_scope, nonce=_nonce_hash(nonce), **kwargs)
189189
flow["nonce"] = nonce
190+
if kwargs.get("max_age") is not None:
191+
flow["max_age"] = kwargs["max_age"]
190192
return flow
191193

192194
def obtain_token_by_auth_code_flow(self, auth_code_flow, auth_response, **kwargs):
@@ -208,6 +210,26 @@ def obtain_token_by_auth_code_flow(self, auth_code_flow, auth_response, **kwargs
208210
raise RuntimeError(
209211
'The nonce in id token ("%s") should match our nonce ("%s")' %
210212
(nonce_in_id_token, expected_hash))
213+
214+
if auth_code_flow.get("max_age") is not None:
215+
auth_time = result.get("id_token_claims", {}).get("auth_time")
216+
if not auth_time:
217+
raise RuntimeError(
218+
"13. max_age was requested, ID token should contain auth_time")
219+
now = int(time.time())
220+
skew = 120 # 2 minutes. Hardcoded, for now
221+
if now - skew > auth_time + auth_code_flow["max_age"]:
222+
raise RuntimeError(
223+
"13. auth_time ({auth_time}) was requested, "
224+
"by using max_age ({max_age}) parameter, "
225+
"and now ({now}) too much time has elasped "
226+
"since last end-user authentication. "
227+
"The ID token was: {id_token}".format(
228+
auth_time=auth_time,
229+
max_age=auth_code_flow["max_age"],
230+
now=now,
231+
id_token=json.dumps(result["id_token_claims"], indent=2),
232+
))
211233
return result
212234

213235
def obtain_token_by_browser(

0 commit comments

Comments
 (0)