Skip to content

Commit 3d24f53

Browse files
authored
Merge pull request #195 from AzureAD/release-1.3.0
MSAL Python 1.3.0
2 parents 6bade9f + c789546 commit 3d24f53

15 files changed

+688
-255
lines changed

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Microsoft Authentication Library (MSAL) for Python
22

33

4-
| `dev` branch | Reference Docs
5-
|---------------|---------------
6-
[![Build status](https://api.travis-ci.org/AzureAD/microsoft-authentication-library-for-python.svg?branch=dev)](https://travis-ci.org/AzureAD/microsoft-authentication-library-for-python) | [![Documentation Status](https://readthedocs.org/projects/msal-python/badge/?version=latest)](https://msal-python.readthedocs.io/en/latest/?badge=latest)
4+
| `dev` branch | Reference Docs | # of Downloads
5+
|---------------|---------------|----------------|
6+
[![Build status](https://api.travis-ci.org/AzureAD/microsoft-authentication-library-for-python.svg?branch=dev)](https://travis-ci.org/AzureAD/microsoft-authentication-library-for-python) | [![Documentation Status](https://readthedocs.org/projects/msal-python/badge/?version=latest)](https://msal-python.readthedocs.io/en/latest/?badge=latest) | [![Download monthly](https://pepy.tech/badge/msal/month)](https://pypistats.org/packages/msal)
77

88
The Microsoft Authentication Library for Python enables applications to integrate with the [Microsoft identity platform](https://aka.ms/aaddevv2). It allows you to sign in users or apps with Microsoft identities ([Azure AD](https://azure.microsoft.com/services/active-directory/), [Microsoft Accounts](https://account.microsoft.com) and [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/) accounts) and obtain tokens to call Microsoft APIs such as [Microsoft Graph](https://graph.microsoft.io/) or your own APIs registered with the Microsoft identity platform. It is built using industry standard OAuth2 and OpenID Connect protocols
99

@@ -35,6 +35,11 @@ Before using MSAL Python (or any MSAL SDKs, for that matter), you will have to
3535
[register your application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-v2-register-an-app).
3636

3737
Acquiring tokens with MSAL Python follows this 3-step pattern.
38+
(Note: That is the high level conceptual pattern.
39+
There will be some variations for different flows. They are demonstrated in
40+
[runnable samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample).
41+
)
42+
3843

3944
1. MSAL proposes a clean separation between
4045
[public client applications, and confidential client applications](https://tools.ietf.org/html/rfc6749#section-2.1).
@@ -43,7 +48,9 @@ Acquiring tokens with MSAL Python follows this 3-step pattern.
4348

4449
```python
4550
from msal import PublicClientApplication
46-
app = PublicClientApplication("your_client_id", authority="...")
51+
app = PublicClientApplication(
52+
"your_client_id",
53+
"authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here")
4754
```
4855

4956
Later, each time you would want an access token, you start by:
@@ -67,7 +74,7 @@ Acquiring tokens with MSAL Python follows this 3-step pattern.
6774
# Assuming the end user chose this one
6875
chosen = accounts[0]
6976
# Now let's try to find a token in cache for this account
70-
result = app.acquire_token_silent(config["scope"], account=chosen)
77+
result = app.acquire_token_silent(["your_scope"], account=chosen)
7178
```
7279

7380
3. Either there is no suitable token in the cache, or you chose to skip the previous step,
@@ -86,9 +93,6 @@ Acquiring tokens with MSAL Python follows this 3-step pattern.
8693
print(result.get("correlation_id")) # You may need this when reporting a bug
8794
```
8895

89-
That is the high level pattern. There will be some variations for different flows. They are demonstrated in
90-
[samples hosted right in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample).
91-
9296
Refer the [Wiki](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki) pages for more details on the MSAL Python functionality and usage.
9397

9498
## Migrating from ADAL

msal/application.py

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
import json
13
import time
24
try: # Python 2
35
from urlparse import urljoin
@@ -19,7 +21,7 @@
1921

2022

2123
# The __init__.py will import this. Not the other way around.
22-
__version__ = "1.2.0"
24+
__version__ = "1.3.0"
2325

2426
logger = logging.getLogger(__name__)
2527

@@ -54,11 +56,11 @@ def decorate_scope(
5456
CLIENT_CURRENT_TELEMETRY = 'x-client-current-telemetry'
5557

5658
def _get_new_correlation_id():
57-
return str(uuid.uuid4())
59+
return str(uuid.uuid4())
5860

5961

6062
def _build_current_telemetry_request_header(public_api_id, force_refresh=False):
61-
return "1|{},{}|".format(public_api_id, "1" if force_refresh else "0")
63+
return "1|{},{}|".format(public_api_id, "1" if force_refresh else "0")
6264

6365

6466
def extract_certs(public_cert_content):
@@ -92,12 +94,14 @@ def __init__(
9294
self, client_id,
9395
client_credential=None, authority=None, validate_authority=True,
9496
token_cache=None,
97+
http_client=None,
9598
verify=True, proxies=None, timeout=None,
9699
client_claims=None, app_name=None, app_version=None):
97100
"""Create an instance of application.
98101
99-
:param client_id: Your app has a client_id after you register it on AAD.
100-
:param client_credential:
102+
:param str client_id: Your app has a client_id after you register it on AAD.
103+
104+
:param str client_credential:
101105
For :class:`PublicClientApplication`, you simply use `None` here.
102106
For :class:`ConfidentialClientApplication`,
103107
it can be a string containing client secret,
@@ -114,6 +118,17 @@ def __init__(
114118
which will be sent through 'x5c' JWT header only for
115119
subject name and issuer authentication to support cert auto rolls.
116120
121+
Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_,
122+
"the certificate containing
123+
the public key corresponding to the key used to digitally sign the
124+
JWS MUST be the first certificate. This MAY be followed by
125+
additional certificates, with each subsequent certificate being the
126+
one used to certify the previous one."
127+
However, your certificate's issuer may use a different order.
128+
So, if your attempt ends up with an error AADSTS700027 -
129+
"The provided signature value did not match the expected signature value",
130+
you may try use only the leaf cert (in PEM/str format) instead.
131+
117132
:param dict client_claims:
118133
*Added in version 0.5.0*:
119134
It is a dictionary of extra claims that would be signed by
@@ -139,18 +154,24 @@ def __init__(
139154
:param TokenCache cache:
140155
Sets the token cache used by this ClientApplication instance.
141156
By default, an in-memory cache will be created and used.
157+
:param http_client: (optional)
158+
Your implementation of abstract class HttpClient <msal.oauth2cli.http.http_client>
159+
Defaults to a requests session instance
142160
:param verify: (optional)
143161
It will be passed to the
144162
`verify parameter in the underlying requests library
145163
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_
164+
This does not apply if you have chosen to pass your own Http client
146165
:param proxies: (optional)
147166
It will be passed to the
148167
`proxies parameter in the underlying requests library
149168
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_
169+
This does not apply if you have chosen to pass your own Http client
150170
:param timeout: (optional)
151171
It will be passed to the
152172
`timeout parameter in the underlying requests library
153173
<http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_
174+
This does not apply if you have chosen to pass your own Http client
154175
:param app_name: (optional)
155176
You can provide your application name for Microsoft telemetry purposes.
156177
Default value is None, means it will not be passed to Microsoft.
@@ -161,14 +182,21 @@ def __init__(
161182
self.client_id = client_id
162183
self.client_credential = client_credential
163184
self.client_claims = client_claims
164-
self.verify = verify
165-
self.proxies = proxies
166-
self.timeout = timeout
185+
if http_client:
186+
self.http_client = http_client
187+
else:
188+
self.http_client = requests.Session()
189+
self.http_client.verify = verify
190+
self.http_client.proxies = proxies
191+
# Requests, does not support session - wide timeout
192+
# But you can patch that (https://github.com/psf/requests/issues/3341):
193+
self.http_client.request = functools.partial(
194+
self.http_client.request, timeout=timeout)
167195
self.app_name = app_name
168196
self.app_version = app_version
169197
self.authority = Authority(
170198
authority or "https://login.microsoftonline.com/common/",
171-
validate_authority, verify=verify, proxies=proxies, timeout=timeout)
199+
self.http_client, validate_authority=validate_authority)
172200
# Here the self.authority is not the same type as authority in input
173201
self.token_cache = token_cache or TokenCache()
174202
self.client = self._build_client(client_credential, self.authority)
@@ -211,14 +239,14 @@ def _build_client(self, client_credential, authority):
211239
return Client(
212240
server_configuration,
213241
self.client_id,
242+
http_client=self.http_client,
214243
default_headers=default_headers,
215244
default_body=default_body,
216245
client_assertion=client_assertion,
217246
client_assertion_type=client_assertion_type,
218247
on_obtaining_tokens=self.token_cache.add,
219248
on_removing_rt=self.token_cache.remove_rt,
220-
on_updating_rt=self.token_cache.update_rt,
221-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
249+
on_updating_rt=self.token_cache.update_rt)
222250

223251
def get_authorization_request_url(
224252
self,
@@ -230,6 +258,7 @@ def get_authorization_request_url(
230258
response_type="code", # Can be "token" if you use Implicit Grant
231259
prompt=None,
232260
nonce=None,
261+
domain_hint=None, # type: Optional[str]
233262
**kwargs):
234263
"""Constructs a URL for you to start a Authorization Code Grant.
235264
@@ -251,6 +280,12 @@ def get_authorization_request_url(
251280
:param nonce:
252281
A cryptographically random value used to mitigate replay attacks. See also
253282
`OIDC specs <https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest>`_.
283+
:param domain_hint:
284+
Can be one of "consumers" or "organizations" or your tenant domain "contoso.com".
285+
If included, it will skip the email-based discovery process that user goes
286+
through on the sign-in page, leading to a slightly more streamlined user experience.
287+
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code
288+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8
254289
:return: The authorization url as a string.
255290
"""
256291
""" # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -269,18 +304,20 @@ def get_authorization_request_url(
269304
# Multi-tenant app can use new authority on demand
270305
the_authority = Authority(
271306
authority,
272-
verify=self.verify, proxies=self.proxies, timeout=self.timeout,
307+
self.http_client
273308
) if authority else self.authority
274309

275310
client = Client(
276311
{"authorization_endpoint": the_authority.authorization_endpoint},
277-
self.client_id)
312+
self.client_id,
313+
http_client=self.http_client)
278314
return client.build_auth_request_uri(
279315
response_type=response_type,
280316
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
281317
prompt=prompt,
282318
scope=decorate_scope(scopes, self.client_id),
283319
nonce=nonce,
320+
domain_hint=domain_hint,
284321
)
285322

286323
def acquire_token_by_authorization_code(
@@ -379,13 +416,12 @@ def _find_msal_accounts(self, environment):
379416

380417
def _get_authority_aliases(self, instance):
381418
if not self.authority_groups:
382-
resp = requests.get(
419+
resp = self.http_client.get(
383420
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize",
384-
headers={'Accept': 'application/json'},
385-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
421+
headers={'Accept': 'application/json'})
386422
resp.raise_for_status()
387423
self.authority_groups = [
388-
set(group['aliases']) for group in resp.json()['metadata']]
424+
set(group['aliases']) for group in json.loads(resp.text)['metadata']]
389425
for group in self.authority_groups:
390426
if instance in group:
391427
return [alias for alias in group if alias != instance]
@@ -504,7 +540,7 @@ def acquire_token_silent_with_error(
504540
warnings.warn("We haven't decided how/if this method will accept authority parameter")
505541
# the_authority = Authority(
506542
# authority,
507-
# verify=self.verify, proxies=self.proxies, timeout=self.timeout,
543+
# self.http_client,
508544
# ) if authority else self.authority
509545
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
510546
scopes, account, self.authority, force_refresh=force_refresh,
@@ -516,8 +552,8 @@ def acquire_token_silent_with_error(
516552
for alias in self._get_authority_aliases(self.authority.instance):
517553
the_authority = Authority(
518554
"https://" + alias + "/" + self.authority.tenant,
519-
validate_authority=False,
520-
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
555+
self.http_client,
556+
validate_authority=False)
521557
result = self._acquire_token_silent_from_cache_and_possibly_refresh_it(
522558
scopes, account, the_authority, force_refresh=force_refresh,
523559
correlation_id=correlation_id,
@@ -597,16 +633,18 @@ def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
597633
**kwargs)
598634
if at and "error" not in at:
599635
return at
636+
last_resp = None
600637
if app_metadata.get("family_id"): # Meaning this app belongs to this family
601-
at = self._acquire_token_silent_by_finding_specific_refresh_token(
638+
last_resp = at = self._acquire_token_silent_by_finding_specific_refresh_token(
602639
authority, scopes, dict(query, family_id=app_metadata["family_id"]),
603640
**kwargs)
604641
if at and "error" not in at:
605642
return at
606643
# Either this app is an orphan, so we will naturally use its own RT;
607644
# or all attempts above have failed, so we fall back to non-foci behavior.
608645
return self._acquire_token_silent_by_finding_specific_refresh_token(
609-
authority, scopes, dict(query, client_id=self.client_id), **kwargs)
646+
authority, scopes, dict(query, client_id=self.client_id),
647+
**kwargs) or last_resp
610648

611649
def _get_app_metadata(self, environment):
612650
apps = self.token_cache.find( # Use find(), rather than token_cache.get(...)
@@ -662,6 +700,36 @@ def _validate_ssh_cert_input_data(self, data):
662700
"you must include a string parameter named 'key_id' "
663701
"which identifies the key in the 'req_cnf' argument.")
664702

703+
def acquire_token_by_refresh_token(self, refresh_token, scopes):
704+
"""Acquire token(s) based on a refresh token (RT) obtained from elsewhere.
705+
706+
You use this method only when you have old RTs from elsewhere,
707+
and now you want to migrate them into MSAL.
708+
Calling this method results in new tokens automatically storing into MSAL.
709+
710+
You do NOT need to use this method if you are already using MSAL.
711+
MSAL maintains RT automatically inside its token cache,
712+
and an access token can be retrieved
713+
when you call :func:`~acquire_token_silent`.
714+
715+
:param str refresh_token: The old refresh token, as a string.
716+
717+
:param list scopes:
718+
The scopes associate with this old RT.
719+
Each scope needs to be in the Microsoft identity platform (v2) format.
720+
See `Scopes not resources <https://docs.microsoft.com/en-us/azure/active-directory/develop/migrate-python-adal-msal#scopes-not-resources>`_.
721+
722+
:return:
723+
* A dict contains "error" and some other keys, when error happened.
724+
* A dict contains no "error" key means migration was successful.
725+
"""
726+
return self.client.obtain_token_by_refresh_token(
727+
refresh_token,
728+
decorate_scope(scopes, self.client_id),
729+
rt_getter=lambda rt: rt,
730+
on_updating_rt=False,
731+
)
732+
665733

666734
class PublicClientApplication(ClientApplication): # browser app or mobile app
667735

@@ -760,13 +828,11 @@ def acquire_token_by_username_password(
760828

761829
def _acquire_token_by_username_password_federated(
762830
self, user_realm_result, username, password, scopes=None, **kwargs):
763-
verify = kwargs.pop("verify", self.verify)
764-
proxies = kwargs.pop("proxies", self.proxies)
765831
wstrust_endpoint = {}
766832
if user_realm_result.get("federation_metadata_url"):
767833
wstrust_endpoint = mex_send_request(
768834
user_realm_result["federation_metadata_url"],
769-
verify=verify, proxies=proxies)
835+
self.http_client)
770836
if wstrust_endpoint is None:
771837
raise ValueError("Unable to find wstrust endpoint from MEX. "
772838
"This typically happens when attempting MSA accounts. "
@@ -778,7 +844,7 @@ def _acquire_token_by_username_password_federated(
778844
wstrust_endpoint.get("address",
779845
# Fallback to an AAD supplied endpoint
780846
user_realm_result.get("federation_active_auth_url")),
781-
wstrust_endpoint.get("action"), verify=verify, proxies=proxies)
847+
wstrust_endpoint.get("action"), self.http_client)
782848
if not ("token" in wstrust_result and "type" in wstrust_result):
783849
raise RuntimeError("Unsuccessful RSTR. %s" % wstrust_result)
784850
GRANT_TYPE_SAML1_1 = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer'

0 commit comments

Comments
 (0)