Skip to content

Commit 7856e8a

Browse files
authored
Merge pull request #173 from AzureAD/nonce-in-msal
Specifying and validating nonce in auth code flow
2 parents 6b52b30 + 67f52df commit 7856e8a

File tree

2 files changed

+31
-7
lines changed

2 files changed

+31
-7
lines changed

msal/application.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def get_authorization_request_url(
229229
redirect_uri=None,
230230
response_type="code", # Can be "token" if you use Implicit Grant
231231
prompt=None,
232+
nonce=None,
232233
**kwargs):
233234
"""Constructs a URL for you to start a Authorization Code Grant.
234235
@@ -247,6 +248,9 @@ def get_authorization_request_url(
247248
You will have to specify a value explicitly.
248249
Its valid values are defined in Open ID Connect specs
249250
https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
251+
:param nonce:
252+
A cryptographically random value used to mitigate replay attacks. See also
253+
`OIDC specs <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
250254
:return: The authorization url as a string.
251255
"""
252256
""" # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -276,6 +280,7 @@ def get_authorization_request_url(
276280
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
277281
prompt=prompt,
278282
scope=decorate_scope(scopes, self.client_id),
283+
nonce=nonce,
279284
)
280285

281286
def acquire_token_by_authorization_code(
@@ -286,6 +291,7 @@ def acquire_token_by_authorization_code(
286291
# REQUIRED, if the "redirect_uri" parameter was included in the
287292
# authorization request as described in Section 4.1.1, and their
288293
# values MUST be identical.
294+
nonce=None,
289295
**kwargs):
290296
"""The second half of the Authorization Code Grant.
291297
@@ -306,6 +312,11 @@ def acquire_token_by_authorization_code(
306312
So the developer need to specify a scope so that we can restrict the
307313
token to be issued for the corresponding audience.
308314
315+
:param nonce:
316+
If you provided a nonce when calling :func:`get_authorization_request_url`,
317+
same nonce should also be provided here, so that we'll validate it.
318+
An exception will be raised if the nonce in id token mismatches.
319+
309320
:return: A dict representing the json response from AAD:
310321
311322
- A successful response would contain "access_token" key,
@@ -326,6 +337,7 @@ def acquire_token_by_authorization_code(
326337
CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header(
327338
self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID),
328339
},
340+
nonce=nonce,
329341
**kwargs)
330342

331343
def get_accounts(self, username=None):

tests/test_e2e.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ def _get_app_and_auth_code(
1919
authority="https://login.microsoftonline.com/common",
2020
port=44331,
2121
scopes=["https://graph.microsoft.com/.default"], # Microsoft Graph
22-
):
22+
**kwargs):
2323
from msal.oauth2cli.authcode import obtain_auth_code
2424
app = msal.ClientApplication(client_id, client_secret, authority=authority)
2525
redirect_uri = "http://localhost:%d" % port
2626
ac = obtain_auth_code(port, auth_uri=app.get_authorization_request_url(
27-
scopes, redirect_uri=redirect_uri))
27+
scopes, redirect_uri=redirect_uri, **kwargs))
2828
assert ac is not None
2929
return (app, ac, redirect_uri)
3030

@@ -124,20 +124,20 @@ def test_username_password(self):
124124
self.skipUnlessWithConfig(["client_id", "username", "password", "scope"])
125125
self._test_username_password(**self.config)
126126

127-
def _get_app_and_auth_code(self):
127+
def _get_app_and_auth_code(self, **kwargs):
128128
return _get_app_and_auth_code(
129129
self.config["client_id"],
130130
client_secret=self.config.get("client_secret"),
131131
authority=self.config.get("authority"),
132132
port=self.config.get("listen_port", 44331),
133133
scopes=self.config["scope"],
134-
)
134+
**kwargs)
135135

136-
def test_auth_code(self):
136+
def _test_auth_code(self, auth_kwargs, token_kwargs):
137137
self.skipUnlessWithConfig(["client_id", "scope"])
138-
(self.app, ac, redirect_uri) = self._get_app_and_auth_code()
138+
(self.app, ac, redirect_uri) = self._get_app_and_auth_code(**auth_kwargs)
139139
result = self.app.acquire_token_by_authorization_code(
140-
ac, self.config["scope"], redirect_uri=redirect_uri)
140+
ac, self.config["scope"], redirect_uri=redirect_uri, **token_kwargs)
141141
logger.debug("%s.cache = %s",
142142
self.id(), json.dumps(self.app.token_cache._cache, indent=4))
143143
self.assertIn(
@@ -148,6 +148,18 @@ def test_auth_code(self):
148148
error_description=result.get("error_description")))
149149
self.assertCacheWorksForUser(result, self.config["scope"], username=None)
150150

151+
def test_auth_code(self):
152+
self._test_auth_code({}, {})
153+
154+
def test_auth_code_with_matching_nonce(self):
155+
self._test_auth_code({"nonce": "foo"}, {"nonce": "foo"})
156+
157+
def test_auth_code_with_mismatching_nonce(self):
158+
self.skipUnlessWithConfig(["client_id", "scope"])
159+
(self.app, ac, redirect_uri) = self._get_app_and_auth_code(nonce="foo")
160+
with self.assertRaises(ValueError):
161+
self.app.acquire_token_by_authorization_code(
162+
ac, self.config["scope"], redirect_uri=redirect_uri, nonce="bar")
151163

152164
def test_ssh_cert(self):
153165
self.skipUnlessWithConfig(["client_id", "scope"])

0 commit comments

Comments
 (0)