Skip to content

Commit e43de20

Browse files
authored
MSAL Python 0.3.1
2 parents 7acfdc5 + da81182 commit e43de20

File tree

6 files changed

+126
-40
lines changed

6 files changed

+126
-40
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ by supporting authentication of users with
77
and [Microsoft Accounts](https://account.microsoft.com) using industry standard OAuth2 and OpenID Connect.
88
Soon MSAL Python will also support [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/).
99

10-
More and more detail about MSAL Python functionality and usage will be documented in the
11-
[Wiki](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki).
10+
Not sure whether this is the SDK you are looking for? There are other Microsoft Identity SDKs
11+
[here](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Microsoft-Authentication-Client-Libraries).
1212

1313
## Important Note about the MSAL Preview
1414

@@ -29,7 +29,7 @@ as applications written using a preview version of library may no longer work.
2929
of your Python environment to a recent version. We tested with pip 18.1.
3030
2. As usual, just run `pip install msal`.
3131

32-
## Usage
32+
## Usage and Samples
3333

3434
Before using MSAL Python (or any MSAL SDKs, for that matter), you will have to
3535
[register your application with the AAD 2.0 endpoint](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-register-an-app).
@@ -77,7 +77,7 @@ Acquiring tokens with MSAL Python need to follow this 3-step pattern.
7777
```python
7878
if not result:
7979
# So no suitable token exists in cache. Let's get a new one from AAD.
80-
result = app.acquire_token_by_one_of_the_actual_method(..., scopes=["user.read"])
80+
result = app.acquire_token_by_one_of_the_actual_method(..., scopes=["User.Read"])
8181
if "access_token" in result:
8282
print(result["access_token"]) # Yay!
8383
else:
@@ -86,20 +86,23 @@ Acquiring tokens with MSAL Python need to follow this 3-step pattern.
8686
print(result.get("correlation_id")) # You may need this when reporting a bug
8787
```
8888

89-
That is it. There will be some variations for different flows.
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).
9091

9192

92-
## Samples and Documentation
93+
## Documentation
9394

9495
The generic documents on
9596
[Auth Scenarios](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios)
9697
and
9798
[Auth protocols](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
9899
are recommended reading.
99100

100-
There is also the [API reference of MSAL Python](https://msal-python.rtfd.io).
101+
There is the [API reference of MSAL Python](https://msal-python.rtfd.io) which documents every parameter of each public method.
102+
103+
More and more detail about MSAL Python functionality and usage will be documented in the
104+
[Wiki](https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki).
101105

102-
You can try [runnable samples in this repo](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample).
103106

104107

105108
## Versions

msal/application.py

Lines changed: 39 additions & 29 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.3.0"
21+
__version__ = "0.3.1"
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -104,17 +104,11 @@ def __init__(
104104
# Here the self.authority is not the same type as authority in input
105105
self.token_cache = token_cache or TokenCache()
106106
self.client = self._build_client(client_credential, self.authority)
107-
self.authority_groups = self._get_authority_aliases()
108-
109-
def _get_authority_aliases(self):
110-
resp = requests.get(
111-
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize",
112-
headers={'Accept': 'application/json'})
113-
resp.raise_for_status()
114-
return [set(group['aliases']) for group in resp.json()['metadata']]
107+
self.authority_groups = None
115108

116109
def _build_client(self, client_credential, authority):
117110
client_assertion = None
111+
client_assertion_type = None
118112
default_body = {"client_info": 1}
119113
if isinstance(client_credential, dict):
120114
assert ("private_key" in client_credential
@@ -124,6 +118,7 @@ def _build_client(self, client_credential, authority):
124118
sha1_thumbprint=client_credential.get("thumbprint"))
125119
client_assertion = signer.sign_assertion(
126120
audience=authority.token_endpoint, issuer=self.client_id)
121+
client_assertion_type = Client.CLIENT_ASSERTION_TYPE_JWT
127122
else:
128123
default_body['client_secret'] = client_credential
129124
server_configuration = {
@@ -142,6 +137,7 @@ def _build_client(self, client_credential, authority):
142137
},
143138
default_body=default_body,
144139
client_assertion=client_assertion,
140+
client_assertion_type=client_assertion_type,
145141
on_obtaining_tokens=self.token_cache.add,
146142
on_removing_rt=self.token_cache.remove_rt,
147143
on_updating_rt=self.token_cache.update_rt,
@@ -249,13 +245,10 @@ def get_accounts(self, username=None):
249245
"""
250246
accounts = self._find_msal_accounts(environment=self.authority.instance)
251247
if not accounts: # Now try other aliases of this authority instance
252-
for group in self.authority_groups:
253-
if self.authority.instance in group:
254-
for alias in group:
255-
if alias != self.authority.instance:
256-
accounts = self._find_msal_accounts(environment=alias)
257-
if accounts:
258-
break
248+
for alias in self._get_authority_aliases(self.authority.instance):
249+
accounts = self._find_msal_accounts(environment=alias)
250+
if accounts:
251+
break
259252
if username:
260253
# Federated account["username"] from AAD could contain mixed case
261254
lowercase_username = username.lower()
@@ -274,6 +267,19 @@ def _find_msal_accounts(self, environment):
274267
if a["authority_type"] in (
275268
TokenCache.AuthorityType.ADFS, TokenCache.AuthorityType.MSSTS)]
276269

270+
def _get_authority_aliases(self, instance):
271+
if not self.authority_groups:
272+
resp = requests.get(
273+
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/authorize",
274+
headers={'Accept': 'application/json'})
275+
resp.raise_for_status()
276+
self.authority_groups = [
277+
set(group['aliases']) for group in resp.json()['metadata']]
278+
for group in self.authority_groups:
279+
if instance in group:
280+
return [alias for alias in group if alias != instance]
281+
return []
282+
277283
def acquire_token_silent(
278284
self,
279285
scopes, # type: List[str]
@@ -309,19 +315,15 @@ def acquire_token_silent(
309315
result = self._acquire_token_silent(scopes, account, self.authority, **kwargs)
310316
if result:
311317
return result
312-
for group in self.authority_groups:
313-
if self.authority.instance in group:
314-
for alias in group:
315-
if alias != self.authority.instance:
316-
the_authority = Authority(
317-
"https://" + alias + "/" + self.authority.tenant,
318-
validate_authority=False,
319-
verify=self.verify, proxies=self.proxies,
320-
timeout=self.timeout,)
321-
result = self._acquire_token_silent(
322-
scopes, account, the_authority, **kwargs)
323-
if result:
324-
return result
318+
for alias in self._get_authority_aliases(self.authority.instance):
319+
the_authority = Authority(
320+
"https://" + alias + "/" + self.authority.tenant,
321+
validate_authority=False,
322+
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
323+
result = self._acquire_token_silent(
324+
scopes, account, the_authority, **kwargs)
325+
if result:
326+
return result
325327

326328
def _acquire_token_silent(
327329
self,
@@ -466,6 +468,9 @@ def acquire_token_by_username_password(
466468
self, username, password, scopes=None, **kwargs):
467469
"""Gets a token for a given resource via user credentails.
468470
471+
See this page for constraints of Username Password Flow.
472+
https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
473+
469474
:param str username: Typically a UPN in the form of an email address.
470475
:param str password: The password.
471476
:param list[str] scopes:
@@ -494,6 +499,11 @@ def _acquire_token_by_username_password_federated(
494499
wstrust_endpoint = mex_send_request(
495500
user_realm_result["federation_metadata_url"],
496501
verify=verify, proxies=proxies)
502+
if wstrust_endpoint is None:
503+
raise ValueError("Unable to find wstrust endpoint from MEX. "
504+
"This typically happens when attempting MSA accounts. "
505+
"More details available here. "
506+
"https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication")
497507
logger.debug("wstrust_endpoint = %s", wstrust_endpoint)
498508
wstrust_result = wst_send_request(
499509
username, password, user_realm_result.get("cloud_audience_urn"),

msal/wstrust_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def send_request(
4747
soap_action = Mex.ACTION_2005
4848
elif '/trust/13/usernamemixed' in endpoint_address:
4949
soap_action = Mex.ACTION_13
50-
assert soap_action in (Mex.ACTION_13, Mex.ACTION_2005) # A loose check here
50+
assert soap_action in (Mex.ACTION_13, Mex.ACTION_2005), ( # A loose check here
51+
"Unsupported soap action: %s" % soap_action)
5152
data = _build_rst(
5253
username, password, cloud_audience_urn, endpoint_address, soap_action)
5354
resp = requests.post(endpoint_address, data=data, headers={
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
The configuration file would look like this (sans those // comments):
3+
4+
{
5+
"authority": "https://login.microsoftonline.com/organizations",
6+
"client_id": "your_client_id",
7+
"scope": ["https://graph.microsoft.com/.default"],
8+
// For more information about scopes for an app, refer:
9+
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate"
10+
11+
"thumbprint": "790E... The thumbprint generated by AAD when you upload your public cert",
12+
"private_key_file": "filename.pem"
13+
// For information about generating thumbprint and private key file, refer:
14+
// https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Client-Credentials#client-credentials-with-certificate
15+
}
16+
17+
You can then run this sample with a JSON configuration file:
18+
19+
python sample.py parameters.json
20+
"""
21+
22+
import sys # For simplicity, we'll read config file from 1st CLI param sys.argv[1]
23+
import json
24+
import logging
25+
26+
import msal
27+
28+
29+
# Optional logging
30+
# logging.basicConfig(level=logging.DEBUG)
31+
32+
config = json.load(open(sys.argv[1]))
33+
34+
# Create a preferably long-lived app instance which maintains a token cache.
35+
app = msal.ConfidentialClientApplication(
36+
config["client_id"], authority=config["authority"],
37+
client_credential={"thumbprint": config["thumbprint"], "private_key": open(config['private_key_file']).read()},
38+
# token_cache=... # Default cache is in memory only.
39+
# You can learn how to use SerializableTokenCache from
40+
# https://msal-python.rtfd.io/en/latest/#msal.SerializableTokenCache
41+
)
42+
43+
# The pattern to acquire a token looks like this.
44+
result = None
45+
46+
# Firstly, looks up a token from cache
47+
# Since we are looking for token for the current app, NOT for an end user,
48+
# notice we give account parameter as None.
49+
result = app.acquire_token_silent(config["scope"], account=None)
50+
51+
if not result:
52+
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
53+
result = app.acquire_token_for_client(scopes=config["scope"])
54+
55+
if "access_token" in result:
56+
print(result["access_token"])
57+
print(result["token_type"])
58+
print(result["expires_in"]) # You don't normally need to care about this.
59+
# It will be good for at least 5 minutes.
60+
else:
61+
print(result.get("error"))
62+
print(result.get("error_description"))
63+
print(result.get("correlation_id")) # You may need this when reporting a bug
64+

sample/client_credential_sample.py renamed to sample/confidential_client_secret_sample.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
"""
2-
The configuration file would look like this:
2+
The configuration file would look like this (sans those // comments):
33
44
{
55
"authority": "https://login.microsoftonline.com/organizations",
66
"client_id": "your_client_id",
77
"scope": ["https://graph.microsoft.com/.default"],
8-
"secret": "This is a sample only. You better NOT persist your password."
8+
// For more information about scopes for an app, refer:
9+
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate"
10+
11+
"secret": "The secret generated by AAD during your confidential app registration"
12+
// For information about generating client secret, refer:
13+
// https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Client-Credentials#registering-client-secrets-using-the-application-registration-portal
14+
915
}
1016
1117
You can then run this sample with a JSON configuration file:

sample/username_password_sample.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
if not result:
4747
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
48+
# See this page for constraints of Username Password Flow.
49+
# https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
4850
result = app.acquire_token_by_username_password(
4951
config["username"], config["password"], scopes=config["scope"])
5052

0 commit comments

Comments
 (0)