Skip to content

Commit d0251ee

Browse files
CopilotbgavrilMS
andauthored
fix: remove id token validation logic
Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-for-python/sessions/d56329c6-d8ad-4440-8617-3df24459fed0 Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com>
1 parent a3531b6 commit d0251ee

4 files changed

Lines changed: 59 additions & 108 deletions

File tree

msal/application.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,6 @@ def initiate_auth_code_flow(
960960
If the elapsed time is greater than this value,
961961
Microsoft identity platform will actively re-authenticate the End-User.
962962
963-
MSAL Python will also automatically validate the auth_time in ID token.
964-
965963
New in version 1.15.
966964
967965
:param str response_mode:
@@ -1204,8 +1202,7 @@ def acquire_token_by_authorization_code(
12041202
12051203
:param nonce:
12061204
If you provided a nonce when calling :func:`get_authorization_request_url`,
1207-
same nonce should also be provided here, so that we'll validate it.
1208-
An exception will be raised if the nonce in id token mismatches.
1205+
same nonce can still be provided here for backward compatibility.
12091206
12101207
:param claims_challenge:
12111208
The claims_challenge parameter requests specific claims requested by the resource provider
@@ -2162,8 +2159,6 @@ def acquire_token_interactive(
21622159
If the elapsed time is greater than this value,
21632160
Microsoft identity platform will actively re-authenticate the End-User.
21642161
2165-
MSAL Python will also automatically validate the auth_time in ID token.
2166-
21672162
New in version 1.15.
21682163
21692164
:param int parent_window_handle:

msal/oauth2cli/oidc.py

Lines changed: 8 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -75,61 +75,14 @@ class IdTokenNonceError(IdTokenError):
7575
pass
7676

7777
def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None):
78-
"""Decodes and validates an id_token and returns its claims as a dictionary.
78+
"""Decodes an id_token and returns its claims as a dictionary.
7979
8080
ID token claims would at least contain: "iss", "sub", "aud", "exp", "iat",
8181
per `specs <https://openid.net/specs/openid-connect-core-1_0.html#IDToken>`_
8282
and it may contain other optional content such as "preferred_username",
8383
`maybe more <https://openid.net/specs/openid-connect-core-1_0.html#Claims>`_
8484
"""
85-
decoded = json.loads(decode_part(id_token.split('.')[1]))
86-
# Based on https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
87-
_now = int(now or time.time())
88-
skew = 120 # 2 minutes
89-
90-
if _now + skew < decoded.get("nbf", _now - 1): # nbf is optional per JWT specs
91-
# This is not an ID token validation, but a JWT validation
92-
# https://tools.ietf.org/html/rfc7519#section-4.1.5
93-
_IdTokenTimeError("0. The ID token is not yet valid.", _now, decoded).log()
94-
95-
if issuer and issuer != decoded["iss"]:
96-
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
97-
raise IdTokenIssuerError(
98-
'2. The Issuer Identifier for the OpenID Provider, "%s", '
99-
"(which is typically obtained during Discovery), "
100-
"MUST exactly match the value of the iss (issuer) Claim." % issuer,
101-
_now,
102-
decoded)
103-
104-
if client_id:
105-
valid_aud = client_id in decoded["aud"] if isinstance(
106-
decoded["aud"], list) else client_id == decoded["aud"]
107-
if not valid_aud:
108-
raise IdTokenAudienceError(
109-
"3. The aud (audience) claim must contain this client's client_id "
110-
'"%s", case-sensitively. Was your client_id in wrong casing?'
111-
# Some IdP accepts wrong casing request but issues right casing IDT
112-
% client_id,
113-
_now,
114-
decoded)
115-
116-
# Per specs:
117-
# 6. If the ID Token is received via direct communication between
118-
# the Client and the Token Endpoint (which it is during _obtain_token()),
119-
# the TLS server validation MAY be used to validate the issuer
120-
# in place of checking the token signature.
121-
122-
if _now - skew > decoded["exp"]:
123-
_IdTokenTimeError("9. The ID token already expires.", _now, decoded).log()
124-
125-
if nonce and nonce != decoded.get("nonce"):
126-
raise IdTokenNonceError(
127-
"11. Nonce must be the same value "
128-
"as the one that was sent in the Authentication Request.",
129-
_now,
130-
decoded)
131-
132-
return decoded
85+
return json.loads(decode_part(id_token.split('.')[1]))
13386

13487

13588
def _nonce_hash(nonce):
@@ -158,9 +111,7 @@ class Client(oauth2.Client):
158111

159112
def decode_id_token(self, id_token, nonce=None):
160113
"""See :func:`~decode_id_token`."""
161-
return decode_id_token(
162-
id_token, nonce=nonce,
163-
client_id=self.client_id, issuer=self.configuration.get("issuer"))
114+
return decode_id_token(id_token)
164115

165116
def _obtain_token(self, grant_type, *args, **kwargs):
166117
"""The result will also contain one more key "id_token_claims",
@@ -193,20 +144,14 @@ def obtain_token_by_authorization_code(self, code, nonce=None, **kwargs):
193144
plus new parameter(s):
194145
195146
:param nonce:
196-
If you provided a nonce when calling :func:`build_auth_request_uri`,
197-
same nonce should also be provided here, so that we'll validate it.
198-
An exception will be raised if the nonce in id token mismatches.
147+
Optional. If you provided a nonce when calling
148+
:func:`build_auth_request_uri`, you may still pass it here for
149+
backward compatibility.
199150
"""
200151
warnings.warn(
201152
"Use obtain_token_by_auth_code_flow() instead", DeprecationWarning)
202-
result = super(Client, self).obtain_token_by_authorization_code(
153+
return super(Client, self).obtain_token_by_authorization_code(
203154
code, **kwargs)
204-
nonce_in_id_token = result.get("id_token_claims", {}).get("nonce")
205-
if "id_token_claims" in result and nonce and nonce != nonce_in_id_token:
206-
raise ValueError(
207-
'The nonce in id token ("%s") should match your nonce ("%s")' %
208-
(nonce_in_id_token, nonce))
209-
return result
210155

211156
def initiate_auth_code_flow(
212157
self,
@@ -249,42 +194,13 @@ def obtain_token_by_auth_code_flow(self, auth_code_flow, auth_response, **kwargs
249194
"""Validate the auth_response being redirected back, and then obtain tokens,
250195
including ID token which can be used for user sign in.
251196
252-
Internally, it implements nonce to mitigate replay attack.
253197
It also implements PKCE to mitigate the auth code interception attack.
254198
255199
See :func:`oauth2.Client.obtain_token_by_auth_code_flow` in parent class
256200
for descriptions on other parameters and return value.
257201
"""
258-
result = super(Client, self).obtain_token_by_auth_code_flow(
202+
return super(Client, self).obtain_token_by_auth_code_flow(
259203
auth_code_flow, auth_response, **kwargs)
260-
if "id_token_claims" in result:
261-
nonce_in_id_token = result.get("id_token_claims", {}).get("nonce")
262-
expected_hash = _nonce_hash(auth_code_flow["nonce"])
263-
if nonce_in_id_token != expected_hash:
264-
raise RuntimeError(
265-
'The nonce in id token ("%s") should match our nonce ("%s")' %
266-
(nonce_in_id_token, expected_hash))
267-
268-
if auth_code_flow.get("max_age") is not None:
269-
auth_time = result.get("id_token_claims", {}).get("auth_time")
270-
if not auth_time:
271-
raise RuntimeError(
272-
"13. max_age was requested, ID token should contain auth_time")
273-
now = int(time.time())
274-
skew = 120 # 2 minutes. Hardcoded, for now
275-
if now - skew > auth_time + auth_code_flow["max_age"]:
276-
raise RuntimeError(
277-
"13. auth_time ({auth_time}) was requested, "
278-
"by using max_age ({max_age}) parameter, "
279-
"and now ({now}) too much time has elasped "
280-
"since last end-user authentication. "
281-
"The ID token was: {id_token}".format(
282-
auth_time=auth_time,
283-
max_age=auth_code_flow["max_age"],
284-
now=now,
285-
id_token=json.dumps(result["id_token_claims"], indent=2),
286-
))
287-
return result
288204

289205
def obtain_token_by_browser(
290206
self,
@@ -299,7 +215,6 @@ def obtain_token_by_browser(
299215
**kwargs):
300216
"""A native app can use this method to obtain token via a local browser.
301217
302-
Internally, it implements nonce to mitigate replay attack.
303218
It also implements PKCE to mitigate the auth code interception attack.
304219
305220
:param string display: Defined in
@@ -334,4 +249,3 @@ def obtain_token_by_browser(
334249
return super(Client, self).obtain_token_by_browser(
335250
auth_params=dict(kwargs.pop("auth_params", {}), **filtered_params),
336251
**kwargs)
337-

msal/token_cache.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,7 @@ def __add(self, event, now=None):
343343
refresh_token = response.get("refresh_token")
344344
id_token = response.get("id_token")
345345
id_token_claims = response.get("id_token_claims") or ( # Prefer the claims from broker
346-
# Only use decode_id_token() when necessary, it contains time-sensitive validation
347-
decode_id_token(id_token, client_id=event["client_id"]) if id_token else {})
346+
decode_id_token(id_token) if id_token else {})
348347
client_info, home_account_id = self.__parse_account(response, id_token_claims)
349348

350349
target = ' '.join(sorted(event.get("scope") or [])) # Schema should have required sorting
@@ -548,4 +547,3 @@ def serialize(self):
548547
with self._lock:
549548
self.has_state_changed = False
550549
return json.dumps(self._cache, indent=4)
551-

tests/test_oidc.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import string
2+
from unittest.mock import patch
23

34
from tests import unittest
45

5-
import msal
66
from msal import oauth2cli
77
from msal.oauth2cli.oauth2 import _generate_pkce_code_verifier
88

@@ -89,7 +89,51 @@ def test_id_token_should_tolerate_time_error(self):
8989
"sub": "subject",
9090
}, "id_token is decoded correctly, without raising exception")
9191

92-
def test_id_token_should_error_out_on_client_id_error(self):
93-
with self.assertRaises(msal.IdTokenError):
94-
oauth2cli.oidc.decode_id_token(self.EXPIRED_ID_TOKEN, client_id="not foo")
95-
92+
def test_id_token_should_ignore_validation_parameters(self):
93+
self.assertEqual(oauth2cli.oidc.decode_id_token(
94+
self.EXPIRED_ID_TOKEN,
95+
client_id="not foo",
96+
issuer="not issuer",
97+
nonce="not nonce",
98+
now=0,
99+
), {
100+
"iss": "issuer",
101+
"iat": 1706570732,
102+
"exp": 1674948332, # 2023-1-28
103+
"aud": "foo",
104+
"sub": "subject",
105+
})
106+
107+
def test_obtain_token_by_authorization_code_should_not_validate_nonce(self):
108+
client = oauth2cli.oidc.Client(
109+
{"authorization_endpoint": "https://example.com/auth",
110+
"token_endpoint": "https://example.com/token",
111+
"issuer": "issuer"},
112+
client_id="foo")
113+
result = {"id_token_claims": {"nonce": "unexpected"}}
114+
with patch.object(
115+
oauth2cli.oauth2.Client, "obtain_token_by_authorization_code",
116+
return_value=result) as mocked:
117+
self.assertEqual(
118+
result,
119+
client.obtain_token_by_authorization_code("code", nonce="expected"))
120+
mocked.assert_called_once_with("code")
121+
122+
def test_obtain_token_by_auth_code_flow_should_not_validate_id_token_claims(self):
123+
client = oauth2cli.oidc.Client(
124+
{"authorization_endpoint": "https://example.com/auth",
125+
"token_endpoint": "https://example.com/token",
126+
"issuer": "issuer"},
127+
client_id="foo")
128+
result = {"id_token_claims": {"nonce": "unexpected"}}
129+
with patch.object(
130+
oauth2cli.oauth2.Client, "obtain_token_by_auth_code_flow",
131+
return_value=result) as mocked:
132+
self.assertEqual(
133+
result,
134+
client.obtain_token_by_auth_code_flow(
135+
{"state": "s", "nonce": "expected", "max_age": 1},
136+
{"state": "s", "code": "code"}))
137+
mocked.assert_called_once_with(
138+
{"state": "s", "nonce": "expected", "max_age": 1},
139+
{"state": "s", "code": "code"})

0 commit comments

Comments
 (0)