Skip to content

Commit 7bec95b

Browse files
committed
A generic low level oauth implementation
1 parent d1be5f5 commit 7bec95b

File tree

3 files changed

+152
-11
lines changed

3 files changed

+152
-11
lines changed

msal/application.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import requests
1+
from . import oauth2
2+
from .exceptions import MsalServiceError
23

34

45
class ClientApplication(object):
56
DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common/"
6-
TOKEN_ENDPOINT_PATH = '/oauth2/v2.0/token'
7+
TOKEN_ENDPOINT_PATH = 'oauth2/v2.0/token'
78

89
def __init__(
910
self, client_id,
@@ -35,13 +36,13 @@ def __init__(self, client_id, client_credential, user_token_cache, **kwargs):
3536
self.user_token_cache = user_token_cache
3637
self.app_token_cache = None # TODO
3738

38-
def acquire_token_for_client(self, scope, policy=None):
39-
data = {
40-
'grant_type': 'client_credentials', 'client_id': self.client_id,
41-
'scope': scope}
42-
if True: # TODO: Need to differenciate the certificate use case
43-
data['client_secret'] = self.client_credential
44-
return requests.post(
45-
self.authority + self.TOKEN_ENDPOINT_PATH, params={'p': policy},
46-
headers={'Accept': 'application/json'}, data=data).json()
39+
def acquire_token_for_client(self, scope, policy=''):
40+
result = oauth2.ClientCredentialGrant(
41+
self.client_id,
42+
token_endpoint="%s%s?policy=%s" % (
43+
self.authority, self.TOKEN_ENDPOINT_PATH, policy),
44+
).get_token(scope=scope, client_secret=self.client_credential)
45+
if 'error' in result:
46+
raise MsalServiceError(**result)
47+
return result
4748

msal/exceptions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ def __init__(self, *args, **kwargs):
3232
super(MsalError, self).__init__(self.msg.format(**kwargs), *args)
3333
self.kwargs = kwargs
3434

35+
class MsalServiceError(MsalError):
36+
msg = "{error}: {error_description}"
37+

msal/oauth2.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
try:
2+
from urllib.parse import urlencode, parse_qs
3+
except ImportError:
4+
from urlparse import parse_qs
5+
from urllib import urlencode
6+
7+
import requests
8+
9+
10+
def validate_authorization(params, state=None):
11+
"""A thin helper to examine the authorization being redirected back"""
12+
if not isinstance(params, dict):
13+
params = parse_qs(params)
14+
if params.get('state') != state:
15+
raise ValueError('state mismatch')
16+
return params
17+
18+
19+
class Client(object):
20+
"""This OAuth2 client implementation aims to be spec-compliant, and generic.
21+
22+
https://tools.ietf.org/html/rfc6749
23+
"""
24+
def __init__(
25+
self, client_id,
26+
client_credential=None, # Only needed for Confidential Client
27+
authorization_endpoint=None, token_endpoint=None):
28+
self.client_id = client_id
29+
self.client_credential = client_credential
30+
self.authorization_endpoint = authorization_endpoint
31+
self.token_endpoint = token_endpoint
32+
33+
def authorization_url(self,
34+
response_type, # MUST be set to "code" or "token"
35+
redirect_uri=None,
36+
scope=None,
37+
state=None, # Recommended by the spec
38+
**kwargs):
39+
"""To generate an authorization url, to be visited by resource owner.
40+
41+
:param scope: It is a space-delimited, case-sensitive string.
42+
Some ID provider can accept empty string to represent default scope.
43+
"""
44+
assert response_type and self.client_id
45+
sep = '&' if '?' in self.authorization_endpoint else '?'
46+
params = {
47+
'client_id': self.client_id,
48+
'response_type': response_type,
49+
'redirect_uri': redirect_uri,
50+
'scope': scope,
51+
'state': state,
52+
}
53+
params.update(kwargs)
54+
return "%s%s%s" % (self.authorization_endpoint, sep, urlencode(params))
55+
56+
def get_token(
57+
self, grant_type,
58+
redirect_uri=None,
59+
scope=None, # Not needed in Authorization Code Grant flow
60+
**kwargs):
61+
# Depending on your chosen grant flow, you may need 'code',
62+
# or 'username' & 'password' pairs, or none of them in the parameters
63+
data = {
64+
'client_id': self.client_id, 'grant_type': grant_type,
65+
'scope': scope}
66+
data.update(kwargs)
67+
68+
# Quoted from https://tools.ietf.org/html/rfc6749#section-2.3.1
69+
# Clients in possession of a client password MAY use the HTTP Basic
70+
# authentication.
71+
# Alternatively, (but NOT RECOMMENDED,)
72+
# the authorization server MAY support including the
73+
# client credentials in the request-body using the following
74+
# parameters: client_id, client_secret.
75+
auth = None
76+
if self.client_credential and not 'client_secret' in data:
77+
auth = (self.client_id, self.client_credential) # HTTP Basic Auth
78+
79+
resp = requests.post(
80+
self.token_endpoint, headers={'Accept': 'application/json'},
81+
data=data, auth=auth)
82+
if resp.status_code>=500:
83+
resp.raise_for_status() # TODO: Will probably try to retry here
84+
# The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says
85+
# even an error response will be a valid json structure,
86+
# so we simply return it here, without needing to invent an exception.
87+
return resp.json()
88+
89+
90+
class AuthorizationCodeGrant(Client):
91+
92+
def authorization_url(self, **kwargs):
93+
return super(AuthorizationCodeGrant, self).authorization_url(
94+
'code', **kwargs)
95+
# Later when you receive the redirected feedback,
96+
# validate_authorization() may be handy to check the returned state.
97+
98+
def get_token(self, code, **kwargs):
99+
return super(AuthorizationCodeGrantFlow, self).get_token(
100+
'authorization_code', code=code, **kwargs)
101+
102+
103+
class ImplicitGrant(Client):
104+
"""This class is only for illustrative purpose.
105+
106+
You probably won't implement your ImplicitGrant flow in Python.
107+
"""
108+
109+
def authorization_url(self, **kwargs):
110+
return super(ImplicitGrant, self).authorization_url(
111+
'token', **kwargs)
112+
113+
def get_token(self):
114+
raise NotImplemented("Token is already issued during authorization")
115+
116+
117+
class ResourceOwnerPasswordCredentialsGrant(Client):
118+
119+
def authorization_url(self, **kwargs):
120+
raise NotImplemented(
121+
"You should have obtained resource owner's password, somehow.")
122+
123+
def get_token(self, username, password, **kwargs):
124+
return super(ResourceOwnerPasswordCredentialsGrant, self).get_token(
125+
"password", username=username, password=password, **kwargs)
126+
127+
128+
class ClientCredentialGrant(Client):
129+
def authorization_url(self, **kwargs):
130+
raise NotImplemented(
131+
# Since the client authentication is used as the authorization grant
132+
"No additional authorization request is needed")
133+
134+
def get_token(self, **kwargs):
135+
return super(ClientCredentialGrant, self).get_token(
136+
"client_credentials", **kwargs)
137+

0 commit comments

Comments
 (0)