Skip to content

Commit c319ea3

Browse files
authored
Merge pull request #100 from AzureAD/release-0.7.0
Release 0.7.0
2 parents 4b34fd6 + 8b60fc6 commit c319ea3

File tree

9 files changed

+454
-285
lines changed

9 files changed

+454
-285
lines changed

msal/application.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
# The __init__.py will import this. Not the other way around.
21-
__version__ = "0.6.1"
21+
__version__ = "0.7.0"
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -238,7 +238,7 @@ def acquire_token_by_authorization_code(
238238
# REQUIRED, if the "redirect_uri" parameter was included in the
239239
# authorization request as described in Section 4.1.1, and their
240240
# values MUST be identical.
241-
):
241+
**kwargs):
242242
"""The second half of the Authorization Code Grant.
243243
244244
:param code: The authorization code returned from Authorization Server.
@@ -270,9 +270,11 @@ def acquire_token_by_authorization_code(
270270
# really empty.
271271
assert isinstance(scopes, list), "Invalid parameter type"
272272
return self.client.obtain_token_by_authorization_code(
273-
code, redirect_uri=redirect_uri,
274-
data={"scope": decorate_scope(scopes, self.client_id)},
275-
)
273+
code, redirect_uri=redirect_uri,
274+
data=dict(
275+
kwargs.pop("data", {}),
276+
scope=decorate_scope(scopes, self.client_id)),
277+
**kwargs)
276278

277279
def get_accounts(self, username=None):
278280
"""Get a list of accounts which previously signed in, i.e. exists in cache.
@@ -439,7 +441,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
439441
logger.debug("Cache hit an AT")
440442
return { # Mimic a real response
441443
"access_token": entry["secret"],
442-
"token_type": "Bearer",
444+
"token_type": entry.get("token_type", "Bearer"),
443445
"expires_in": int(expires_in), # OAuth2 specs defines it as int
444446
}
445447
return self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
@@ -551,7 +553,8 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
551553
"""
552554
return self.client.obtain_token_by_device_flow(
553555
flow,
554-
data={"code": flow["device_code"]}, # 2018-10-4 Hack:
556+
data=dict(kwargs.pop("data", {}), code=flow["device_code"]),
557+
# 2018-10-4 Hack:
555558
# during transition period,
556559
# service seemingly need both device_code and code parameter.
557560
**kwargs)
@@ -624,7 +627,7 @@ def _acquire_token_by_username_password_federated(
624627
class ConfidentialClientApplication(ClientApplication): # server-side web app
625628

626629
def acquire_token_for_client(self, scopes, **kwargs):
627-
"""Acquires token from the service for the confidential client.
630+
"""Acquires token for the current confidential client, not for an end user.
628631
629632
:param list[str] scopes: (Required)
630633
Scopes requested to access a protected API (a resource).
@@ -639,6 +642,38 @@ def acquire_token_for_client(self, scopes, **kwargs):
639642
scope=scopes, # This grant flow requires no scope decoration
640643
**kwargs)
641644

642-
def acquire_token_on_behalf_of(self, user_assertion, scopes, authority=None):
643-
raise NotImplementedError()
645+
def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
646+
"""Acquires token using on-behalf-of (OBO) flow.
647+
648+
The current app is a middle-tier service which was called with a token
649+
representing an end user.
650+
The current app can use such token (a.k.a. a user assertion) to request
651+
another token to access downstream web API, on behalf of that user.
652+
See `detail docs here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow>`_ .
653+
654+
The current middle-tier app has no user interaction to obtain consent.
655+
See how to gain consent upfront for your middle-tier app from this article.
656+
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#gaining-consent-for-the-middle-tier-application
657+
658+
:param str user_assertion: The incoming token already received by this app
659+
:param list[str] scopes: Scopes required by downstream API (a resource).
660+
661+
:return: A dict representing the json response from AAD:
662+
663+
- A successful response would contain "access_token" key,
664+
- an error response would contain "error" and usually "error_description".
665+
"""
666+
# The implementation is NOT based on Token Exchange
667+
# https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
668+
return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
669+
user_assertion,
670+
self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs
671+
scope=decorate_scope(scopes, self.client_id), # Decoration is used for:
672+
# 1. Explicitly requesting an RT, without relying on AAD default
673+
# behavior, even though it currently still issues an RT.
674+
# 2. Requesting an IDT (which would otherwise be unavailable)
675+
# so that the calling app could use id_token_claims to implement
676+
# their own cache mapping, which is likely needed in web apps.
677+
data=dict(kwargs.pop("data", {}), requested_token_use="on_behalf_of"),
678+
**kwargs)
644679

msal/token_cache.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,18 +99,30 @@ def find(self, credential_type, target=None, query=None):
9999

100100
def add(self, event, now=None):
101101
# type: (dict) -> None
102-
# event typically contains: client_id, scope, token_endpoint,
103-
# resposne, params, data, grant_type
104-
for sensitive in ("password", "client_secret"):
105-
if sensitive in event.get("data", {}):
106-
# Hide them from accidental exposure in logging
107-
event["data"][sensitive] = "********"
108-
logger.debug("event=%s", json.dumps(
102+
"""Handle a token obtaining event, and add tokens into cache.
103+
104+
Known side effects: This function modifies the input event in place.
105+
"""
106+
def wipe(dictionary, sensitive_fields): # Masks sensitive info
107+
for sensitive in sensitive_fields:
108+
if sensitive in dictionary:
109+
dictionary[sensitive] = "********"
110+
wipe(event.get("data", {}),
111+
("password", "client_secret", "refresh_token", "assertion"))
112+
try:
113+
return self.__add(event, now=now)
114+
finally:
115+
wipe(event.get("response", {}), ("access_token", "refresh_token"))
116+
logger.debug("event=%s", json.dumps(
109117
# We examined and concluded that this log won't have Log Injection risk,
110118
# because the event payload is already in JSON so CR/LF will be escaped.
111-
event, indent=4, sort_keys=True,
112-
default=str, # A workaround when assertion is in bytes in Python 3
113-
))
119+
event, indent=4, sort_keys=True,
120+
default=str, # A workaround when assertion is in bytes in Python 3
121+
))
122+
123+
def __add(self, event, now=None):
124+
# event typically contains: client_id, scope, token_endpoint,
125+
# response, params, data, grant_type
114126
environment = realm = None
115127
if "token_endpoint" in event:
116128
_, environment, realm = canonicalize(event["token_endpoint"])
@@ -148,6 +160,7 @@ def add(self, event, now=None):
148160
"client_id": event.get("client_id"),
149161
"target": target,
150162
"realm": realm,
163+
"token_type": response.get("token_type", "Bearer"),
151164
"cached_at": str(now), # Schema defines it as a string
152165
"expires_on": str(now + expires_in), # Same here
153166
"extended_expires_on": str(now + ext_expires_in) # Same here
Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,3 @@
1-
"""
2-
The configuration file would look like this:
3-
4-
{
5-
"authority": "https://login.microsoftonline.com/organizations",
6-
"client_id": "your_client_id",
7-
"scope": ["https://graph.microsoft.com/.default"],
8-
"redirect_uri": "http://localhost:5000/getAToken",
9-
// Configure this redirect uri for this sample
10-
// redirect_uri should match what you've configured in here
11-
// https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-redirect-uris-to-your-application
12-
"client_secret": "yoursecret"
13-
}
14-
15-
You can then run this sample with a JSON configuration file:
16-
17-
python sample.py parameters.json your_flask_session_secret_here
18-
19-
And the on the browser open http://localhost:5000/
20-
21-
"""
22-
23-
import sys # For simplicity, we'll read config file from 1st CLI param sys.argv[1]
24-
import json
25-
import logging
26-
import uuid
27-
import os
28-
29-
import flask
30-
31-
import msal
32-
33-
app = flask.Flask(__name__)
34-
app.debug = True
35-
app.secret_key = os.environ.get("FLASK_SECRET")
36-
assert app.secret_key, "This sample requires a FLASK_SECRET env var to enable session"
37-
38-
39-
# Optional logging
40-
# logging.basicConfig(level=logging.DEBUG)
41-
42-
config = json.load(open(sys.argv[1]))
43-
44-
application = msal.ConfidentialClientApplication(
45-
config["client_id"], authority=config["authority"],
46-
client_credential=config["client_secret"],
47-
# token_cache=... # Default cache is in memory only.
48-
# You can learn how to use SerializableTokenCache from
49-
# https://msal-python.rtfd.io/en/latest/#msal.SerializableTokenCache
50-
)
51-
52-
53-
@app.route("/")
54-
def main():
55-
resp = flask.Response(status=307)
56-
resp.headers['location'] = '/login'
57-
return resp
58-
59-
60-
@app.route("/login")
61-
def login():
62-
auth_state = str(uuid.uuid4())
63-
flask.session['state'] = auth_state
64-
authorization_url = application.get_authorization_request_url(config['scope'], state=auth_state,
65-
redirect_uri=config['redirect_uri'])
66-
resp = flask.Response(status=307)
67-
resp.headers['location'] = authorization_url
68-
return resp
69-
70-
71-
@app.route("/getAToken")
72-
def main_logic():
73-
code = flask.request.args['code']
74-
state = flask.request.args['state']
75-
if state != flask.session['state']:
76-
raise ValueError("State does not match")
77-
78-
result = application.acquire_token_by_authorization_code(code, scopes=config["scope"],
79-
redirect_uri=config['redirect_uri'])
80-
return flask.render_template('display.html', auth_result=result)
81-
82-
83-
if __name__ == "__main__":
84-
app.run()
1+
# We have moved!
2+
#
3+
# Please visit https://github.com/Azure-Samples/ms-identity-python-webapp

sample/authorization-code-flow-sample/templates/display.html

Lines changed: 0 additions & 19 deletions
This file was deleted.

sample/username_password_sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@
6161
print(result.get("correlation_id")) # You may need this when reporting a bug
6262
if 65001 in result.get("error_codes", []): # Not mean to be coded programatically, but...
6363
# AAD requires user consent for U/P flow
64-
print("Visit this to consent:", app.get_authorization_request_url(scope))
64+
print("Visit this to consent:", app.get_authorization_request_url(config["scope"]))

0 commit comments

Comments
 (0)