Skip to content

Commit 323d04d

Browse files
committed
Refactor OAuth2 interface
1 parent 17bba2c commit 323d04d

File tree

1 file changed

+56
-64
lines changed

1 file changed

+56
-64
lines changed

msal/oauth2.py

Lines changed: 56 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"""This OAuth2 client implementation aims to be spec-compliant, and generic."""
2+
# OAuth2 spec https://tools.ietf.org/html/rfc6749
3+
14
try:
25
from urllib.parse import urlencode, parse_qs
36
except ImportError:
@@ -7,20 +10,9 @@
710
import requests
811

912

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-
1913
class Client(object):
20-
"""This OAuth2 client implementation aims to be spec-compliant, and generic.
21-
22-
https://tools.ietf.org/html/rfc6749
23-
"""
14+
# This low-level interface works. Yet you'll find those *Grant sub-classes
15+
# more friendly to remind you what parameters are needed in each scenario.
2416
def __init__(
2517
self, client_id,
2618
client_credential=None, # Only needed for Confidential Client
@@ -30,40 +22,15 @@ def __init__(
3022
self.authorization_endpoint = authorization_endpoint
3123
self.token_endpoint = token_endpoint
3224

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-
}
25+
def authorization_url(self, response_type, **kwargs):
26+
params = {'client_id': self.client_id, 'response_type': response_type}
5327
params.update(kwargs)
5428
params = {k: v for k, v in params.items() if v is not None} # clean up
29+
sep = '&' if '?' in self.authorization_endpoint else '?'
5530
return "%s%s%s" % (self.authorization_endpoint, sep, urlencode(params))
5631

57-
def get_token(
58-
self, grant_type,
59-
redirect_uri=None,
60-
scope=None, # Not needed in Authorization Code Grant flow
61-
**kwargs):
62-
# Depending on your chosen grant flow, you may need 'code',
63-
# or 'username' & 'password' pairs, or none of them in the parameters
64-
data = {
65-
'client_id': self.client_id, 'grant_type': grant_type,
66-
'scope': scope}
32+
def get_token(self, grant_type, **kwargs):
33+
data = {'client_id': self.client_id, 'grant_type': grant_type}
6734
data.update(kwargs)
6835
# We don't need to clean up None values here, because requests lib will.
6936

@@ -82,7 +49,7 @@ def get_token(
8249
self.token_endpoint, headers={'Accept': 'application/json'},
8350
data=data, auth=auth)
8451
if resp.status_code>=500:
85-
resp.raise_for_status() # TODO: Will probably try to retry here
52+
resp.raise_for_status() # TODO: Will probably retry here
8653
# The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says
8754
# even an error response will be a valid json structure,
8855
# so we simply return it here, without needing to invent an exception.
@@ -91,26 +58,51 @@ def get_token(
9158

9259
class AuthorizationCodeGrant(Client):
9360

94-
def authorization_url(self, **kwargs):
61+
def authorization_url(
62+
self, redirect_uri=None, scope=None, state=None, **kwargs):
63+
"""Generate an authorization url to be visited by resource owner.
64+
65+
:param response_type: MUST be set to "code" or "token".
66+
:param scope: It is a space-delimited, case-sensitive string.
67+
Some ID provider can accept empty string to represent default scope.
68+
"""
9569
return super(AuthorizationCodeGrant, self).authorization_url(
96-
'code', **kwargs)
97-
# Later when you receive the redirected feedback,
70+
'code', redirect_uri=redirect_uri, scope=scope, state=state,
71+
**kwargs)
72+
# Later when you receive the response at your redirect_uri,
9873
# validate_authorization() may be handy to check the returned state.
9974

100-
def get_token(self, code, **kwargs):
75+
def get_token(self, code, redirect_uri=None, client_id=None, **kwargs):
76+
"""Get an access token.
77+
78+
See also https://tools.ietf.org/html/rfc6749#section-4.1.3
79+
80+
:param code: The authorization code received from authorization server.
81+
:param redirect_uri:
82+
Required, if the "redirect_uri" parameter was included in the
83+
authorization request, and their values MUST be identical.
84+
:param client_id: Required, if the client is not authenticating itself.
85+
See https://tools.ietf.org/html/rfc6749#section-3.2.1
86+
"""
10187
return super(AuthorizationCodeGrantFlow, self).get_token(
102-
'authorization_code', code=code, **kwargs)
88+
'authorization_code', code=code,
89+
redirect_uri=redirect_uri, client_id=client_id, **kwargs)
10390

10491

105-
class ImplicitGrant(Client):
106-
"""This class is only for illustrative purpose.
92+
def validate_authorization(params, state=None):
93+
"""A thin helper to examine the authorization being redirected back"""
94+
if not isinstance(params, dict):
95+
params = parse_qs(params)
96+
if params.get('state') != state:
97+
raise ValueError('state mismatch')
98+
return params
10799

108-
You probably won't implement your ImplicitGrant flow in Python.
109-
"""
110100

111-
def authorization_url(self, **kwargs):
112-
return super(ImplicitGrant, self).authorization_url(
113-
'token', **kwargs)
101+
class ImplicitGrant(Client):
102+
# This class is only for illustrative purpose.
103+
# You probably won't implement your ImplicitGrant flow in Python anyway.
104+
def authorization_url(self, redirect_uri=None, scope=None, state=None):
105+
return super(ImplicitGrant, self).authorization_url('token', **locals())
114106

115107
def get_token(self):
116108
raise NotImplemented("Token is already issued during authorization")
@@ -120,20 +112,20 @@ class ResourceOwnerPasswordCredentialsGrant(Client):
120112

121113
def authorization_url(self, **kwargs):
122114
raise NotImplemented(
123-
"You should have obtained resource owner's password, somehow.")
115+
"You should have already obtained resource owner's password")
124116

125-
def get_token(self, username, password, **kwargs):
117+
def get_token(self, username, password, scope=None, **kwargs):
126118
return super(ResourceOwnerPasswordCredentialsGrant, self).get_token(
127-
"password", username=username, password=password, **kwargs)
119+
"password", username=username, password=password, scope=scope,
120+
**kwargs)
128121

129122

130123
class ClientCredentialGrant(Client):
131124
def authorization_url(self, **kwargs):
132-
raise NotImplemented(
133-
# Since the client authentication is used as the authorization grant
134-
"No additional authorization request is needed")
125+
# Since the client authentication is used as the authorization grant
126+
raise NotImplemented("No additional authorization request is needed")
135127

136-
def get_token(self, **kwargs):
128+
def get_token(self, scope=None, **kwargs):
137129
return super(ClientCredentialGrant, self).get_token(
138-
"client_credentials", **kwargs)
130+
"client_credentials", scope=scope, **kwargs)
139131

0 commit comments

Comments
 (0)