Skip to content

Commit 210103c

Browse files
authored
Merge pull request #63 from AzureAD/release-0.4.1
The red cross i.e. a CI test failure when merging previous PR 61 into dev branch, was a rare intermittent failure in the CI side. In fact, its previous CI test and its subsequent merging into dev branch have all been successful. Therefore we ignore that red cross, and still proceed with our release. Recording such reasoning here for future reference.
2 parents fa88282 + 74df5ee commit 210103c

File tree

9 files changed

+185
-133
lines changed

9 files changed

+185
-133
lines changed

msal/application.py

Lines changed: 4 additions & 3 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.4.0"
21+
__version__ = "0.4.1"
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -271,7 +271,8 @@ def _get_authority_aliases(self, instance):
271271
if not self.authority_groups:
272272
resp = requests.get(
273273
"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'})
274+
headers={'Accept': 'application/json'},
275+
verify=self.verify, proxies=self.proxies, timeout=self.timeout)
275276
resp.raise_for_status()
276277
self.authority_groups = [
277278
set(group['aliases']) for group in resp.json()['metadata']]
@@ -511,7 +512,7 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
511512
**kwargs)
512513

513514
def acquire_token_by_username_password(
514-
self, username, password, scopes=None, **kwargs):
515+
self, username, password, scopes, **kwargs):
515516
"""Gets a token for a given resource via user credentails.
516517
517518
See this page for constraints of Username Password Flow.

msal/authority.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import re
2+
import logging
23

34
import requests
45

56
from .exceptions import MsalServiceError
67

78

9+
logger = logging.getLogger(__name__)
810
WORLD_WIDE = 'login.microsoftonline.com' # There was an alias login.windows.net
911
WELL_KNOWN_AUTHORITY_HOSTS = set([
1012
WORLD_WIDE,
@@ -38,14 +40,15 @@ def __init__(self, authority_url, validate_authority=True,
3840
canonicalized, self.instance, tenant = canonicalize(authority_url)
3941
tenant_discovery_endpoint = ( # Hard code a V2 pattern as default value
4042
'https://{}/{}/v2.0/.well-known/openid-configuration'
41-
.format(WORLD_WIDE, tenant))
43+
.format(self.instance, tenant))
4244
if validate_authority and self.instance not in WELL_KNOWN_AUTHORITY_HOSTS:
4345
tenant_discovery_endpoint = instance_discovery(
4446
canonicalized + "/oauth2/v2.0/authorize",
4547
verify=verify, proxies=proxies, timeout=timeout)
4648
openid_config = tenant_discovery(
4749
tenant_discovery_endpoint,
4850
verify=verify, proxies=proxies, timeout=timeout)
51+
logger.debug("openid_config = %s", openid_config)
4952
self.authorization_endpoint = openid_config['authorization_endpoint']
5053
self.token_endpoint = openid_config['token_endpoint']
5154
_, _, self.tenant = canonicalize(self.token_endpoint) # Usually a GUID
@@ -65,7 +68,7 @@ def user_realm_discovery(self, username):
6568

6669
def canonicalize(url):
6770
# Returns (canonicalized_url, netloc, tenant). Raises ValueError on errors.
68-
match_object = re.match("https://([^/]+)/([^/\?#]+)", url.lower())
71+
match_object = re.match(r'https://([^/]+)/([^/?#]+)', url.lower())
6972
if not match_object:
7073
raise ValueError(
7174
"Your given address (%s) should consist of "
@@ -76,7 +79,11 @@ def canonicalize(url):
7679
def instance_discovery(url, response=None, **kwargs):
7780
# Returns tenant discovery endpoint
7881
resp = requests.get( # Note: This URL seemingly returns V1 endpoint only
79-
'https://{}/common/discovery/instance'.format(WORLD_WIDE),
82+
'https://{}/common/discovery/instance'.format(
83+
WORLD_WIDE # Historically using WORLD_WIDE. Could use self.instance too
84+
# See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadInstanceDiscovery.cs#L101-L103
85+
# and https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.0.0/src/Microsoft.Identity.Client/Instance/AadAuthority.cs#L19-L33
86+
),
8087
params={'authorization_endpoint': url, 'api-version': '1.0'},
8188
**kwargs)
8289
payload = response or resp.json()

msal/oauth2cli/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
__version__ = "0.1.0"
1+
__version__ = "0.2.0"
22

3-
from .oauth2 import Client
3+
from .oidc import Client
44
from .assertion import JwtSigner
55

msal/oauth2cli/oidc.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import json
2+
import base64
3+
import time
4+
5+
from . import oauth2
6+
7+
8+
def base64decode(raw):
9+
"""A helper can handle a padding-less raw input"""
10+
raw += '=' * (-len(raw) % 4) # https://stackoverflow.com/a/32517907/728675
11+
return base64.b64decode(raw).decode("utf-8")
12+
13+
14+
def decode_id_token(id_token, client_id=None, issuer=None, nonce=None, now=None):
15+
"""Decodes and validates an id_token and returns its claims as a dictionary.
16+
17+
ID token claims would at least contain: "iss", "sub", "aud", "exp", "iat",
18+
per `specs <https://openid.net/specs/openid-connect-core-1_0.html#IDToken>`_
19+
and it may contain other optional content such as "preferred_username",
20+
`maybe more <https://openid.net/specs/openid-connect-core-1_0.html#Claims>`_
21+
"""
22+
decoded = json.loads(base64decode(id_token.split('.')[1]))
23+
err = None # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
24+
if issuer and issuer != decoded["iss"]:
25+
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
26+
err = ('2. The Issuer Identifier for the OpenID Provider, "%s", '
27+
"(which is typically obtained during Discovery), "
28+
"MUST exactly match the value of the iss (issuer) Claim.") % issuer
29+
if client_id:
30+
valid_aud = client_id in decoded["aud"] if isinstance(
31+
decoded["aud"], list) else client_id == decoded["aud"]
32+
if not valid_aud:
33+
err = "3. The aud (audience) Claim must contain this client's client_id."
34+
# Per specs:
35+
# 6. If the ID Token is received via direct communication between
36+
# the Client and the Token Endpoint (which it is in this flow),
37+
# the TLS server validation MAY be used to validate the issuer
38+
# in place of checking the token signature.
39+
if (now or time.time()) > decoded["exp"]:
40+
err = "9. The current time MUST be before the time represented by the exp Claim."
41+
if nonce and nonce != decoded.get("nonce"):
42+
err = ("11. Nonce must be the same value "
43+
"as the one that was sent in the Authentication Request")
44+
if err:
45+
raise RuntimeError("%s id_token was: %s" % (
46+
err, json.dumps(decoded, indent=2)))
47+
return decoded
48+
49+
50+
class Client(oauth2.Client):
51+
"""OpenID Connect is a layer on top of the OAuth2.
52+
53+
See its specs at https://openid.net/connect/
54+
"""
55+
56+
def decode_id_token(self, id_token, nonce=None):
57+
"""See :func:`~decode_id_token`."""
58+
return decode_id_token(
59+
id_token, nonce=nonce,
60+
client_id=self.client_id, issuer=self.configuration.get("issuer"))
61+
62+
def _obtain_token(self, grant_type, *args, **kwargs):
63+
"""The result will also contain one more key "id_token_claims",
64+
whose value will be a dictionary returned by :func:`~decode_id_token`.
65+
"""
66+
ret = super(Client, self)._obtain_token(grant_type, *args, **kwargs)
67+
if "id_token" in ret:
68+
ret["id_token_claims"] = self.decode_id_token(ret["id_token"])
69+
return ret
70+

0 commit comments

Comments
 (0)