Skip to content

Commit d512539

Browse files
committed
Based on previous experiment, now decide to implement oauth2 Client as one class, rather than a family of classes
1 parent 020cb38 commit d512539

File tree

3 files changed

+47
-53
lines changed

3 files changed

+47
-53
lines changed

msal/application.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def acquire_token_silent(
4141
default_body=self._build_auth_parameters(
4242
self.client_credential,
4343
the_authority.token_endpoint, self.client_id)
44-
).get_token_by_refresh_token(
44+
).acquire_token_with_refresh_token(
4545
refresh_token,
4646
scope=decorate_scope(scope, self.client_id, policy),
4747
query={'p': policy} if policy else None)
@@ -98,11 +98,11 @@ def __init__(
9898

9999
def acquire_token_for_client(self, scope, policy=None):
100100
token_endpoint = self.authority.token_endpoint
101-
return oauth2.ClientCredentialGrant(
101+
return oauth2.Client(
102102
self.client_id, token_endpoint=token_endpoint,
103103
default_body=self._build_auth_parameters(
104104
self.client_credential, token_endpoint, self.client_id)
105-
).get_token(
105+
).acquire_token_with_client_credentials(
106106
scope=scope, # This grant flow requires no scope decoration
107107
query={'p': policy} if policy else None)
108108

@@ -131,16 +131,17 @@ def get_authorization_request_url(
131131
:param str state: Recommended by OAuth2 for CSRF protection.
132132
"""
133133
the_authority = Authority(authority) if authority else self.authority
134-
grant = oauth2.AuthorizationCodeGrant(
134+
client = oauth2.Client(
135135
self.client_id,
136136
authorization_endpoint=the_authority.authorization_endpoint)
137-
return grant.authorization_url(
137+
return client.authorization_url(
138+
response_type="code", # Using Authorization Code grant
138139
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
139140
scope=decorate_scope(scope, self.client_id, policy),
140141
policy=policy if policy else None,
141142
**(extra_query_params or {}))
142143

143-
def acquire_token_by_authorization_code(
144+
def acquire_token_with_authorization_code(
144145
self,
145146
code,
146147
scope, # Syntactically required. STS accepts empty value though.
@@ -173,12 +174,12 @@ def acquire_token_by_authorization_code(
173174
# So in theory, you can omit scope here when you were working with only
174175
# one scope. But, MSAL decorates your scope anyway, so they are never
175176
# really empty.
176-
return oauth2.AuthorizationCodeGrant(
177+
return oauth2.Client(
177178
self.client_id, token_endpoint=self.authority.token_endpoint,
178179
default_body=self._build_auth_parameters(
179180
self.client_credential,
180181
self.authority.token_endpoint, self.client_id)
181-
).get_token(
182+
).acquire_token_with_authorization_code(
182183
code, redirect_uri=redirect_uri,
183184
scope=decorate_scope(scope, self.client_id, policy),
184185
query={'p': policy} if policy else None)

msal/oauth2.py

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import requests
1111

1212

13-
class Client(object):
14-
# This low-level interface works. Yet you'll find those *Grant sub-classes
13+
class BaseClient(object):
14+
# This low-level interface works. Yet you'll find its sub-class
1515
# more friendly to remind you what parameters are needed in each scenario.
1616
# More on Client Types at https://tools.ietf.org/html/rfc6749#section-2.1
1717
def __init__(
@@ -74,7 +74,8 @@ def _get_token(
7474
# so we simply return it here, without needing to invent an exception.
7575
return resp.json()
7676

77-
def get_token_by_refresh_token(self, refresh_token, scope=None, **kwargs):
77+
def acquire_token_with_refresh_token(
78+
self, refresh_token, scope=None, **kwargs):
7879
return self._get_token(
7980
"refresh_token", refresh_token=refresh_token, scope=scope, **kwargs)
8081

@@ -84,28 +85,33 @@ def _normalize_to_string(self, scope):
8485
return scope # as-is
8586

8687

87-
class AuthorizationCodeGrant(Client):
88-
# Can be used by Confidential Client or Public Client.
89-
# See https://tools.ietf.org/html/rfc6749#section-4.1.3
90-
88+
class Client(BaseClient):
9189
def authorization_url(
92-
self, redirect_uri=None, scope=None, state=None, **kwargs):
90+
self,
91+
response_type, redirect_uri=None, scope=None, state=None, **kwargs):
9392
"""Generate an authorization url to be visited by resource owner.
9493
94+
:param response_type:
95+
Must be "code" when you are using Authorization Code Grant.
96+
Must be "token" when you are using Implicit Grant
9597
:param redirect_uri: Optional. Server will use the pre-registered one.
9698
:param scope: It is a space-delimited, case-sensitive string.
9799
Some ID provider can accept empty string to represent default scope.
98100
"""
101+
assert response_type in ["code", "token"]
99102
return self._authorization_url(
100-
'code', redirect_uri=redirect_uri, scope=scope, state=state,
103+
response_type, redirect_uri=redirect_uri, scope=scope, state=state,
101104
**kwargs)
102105
# Later when you receive the response at your redirect_uri,
103106
# validate_authorization() may be handy to check the returned state.
104107

105-
def get_token(self, code, redirect_uri=None, **kwargs):
106-
"""Get an access token.
108+
def acquire_token_with_authorization_code(
109+
self, code, redirect_uri=None, **kwargs):
110+
"""Get a token via auhtorization code. a.k.a. Authorization Code Grant.
107111
108-
See also https://tools.ietf.org/html/rfc6749#section-4.1.3
112+
This is typically used by a server-side app (Confidential Client),
113+
but it can also be used by a device-side native app (Public Client).
114+
See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3
109115
110116
:param code: The authorization code received from authorization server.
111117
:param redirect_uri:
@@ -118,42 +124,29 @@ def get_token(self, code, redirect_uri=None, **kwargs):
118124
'authorization_code', code=code,
119125
redirect_uri=redirect_uri, **kwargs)
120126

121-
122-
def validate_authorization(params, state=None):
123-
"""A thin helper to examine the authorization being redirected back"""
124-
if not isinstance(params, dict):
125-
params = parse_qs(params)
126-
if params.get('state') != state:
127-
raise ValueError('state mismatch')
128-
return params
129-
130-
131-
class ImplicitGrant(Client):
132-
"""Implicit Grant is used to obtain access tokens (but not refresh token).
133-
134-
It is optimized for public clients known to operate a particular
135-
redirection URI. These clients are typically implemented in a browser
136-
using a scripting language such as JavaScript.
137-
Quoted from https://tools.ietf.org/html/rfc6749#section-4.2
138-
"""
139-
def authorization_url(self, redirect_uri=None, scope=None, state=None):
140-
return self._authorization_url('token', **locals())
141-
142-
143-
class ResourceOwnerPasswordCredentialsGrant(Client): # Legacy Application flow
144-
def get_token(self, username, password, scope=None, **kwargs):
127+
def acquire_token_with_username_password(
128+
self, username, password, scope=None, **kwargs):
129+
"""The Resource Owner Password Credentials Grant, used by legacy app."""
145130
return self._get_token(
146131
"password", username=username, password=password, scope=scope,
147132
**kwargs)
148133

134+
def acquire_token_with_client_credentials(self, scope=None, **kwargs):
135+
'''Get token by client credentials. a.k.a. Client Credentials Grant,
136+
used by Backend Applications.
149137
150-
class ClientCredentialGrant(Client): # a.k.a. Backend Application flow
151-
def get_token(self, scope=None, **kwargs):
152-
'''Get token by client credential.
153-
154-
You may want to also provide an optional client_secret parameter,
138+
You may want to explicitly provide an optional client_secret parameter,
155139
or you can provide such extra parameters as `default_body` during the
156140
class initialization.
157141
'''
158142
return self._get_token("client_credentials", scope=scope, **kwargs)
159143

144+
145+
def validate_authorization(params, state=None):
146+
"""A thin helper to examine the authorization being redirected back"""
147+
if not isinstance(params, dict):
148+
params = parse_qs(params)
149+
if params.get('state') != state:
150+
raise ValueError('state mismatch')
151+
return params
152+

tests/test_application.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
CONFIG_FILE = os.path.join(THIS_FOLDER, 'config.json')
1313

1414

15-
def acquire_token_by_authorization_code(app, redirect_port, scope):
15+
def acquire_token_with_authorization_code(app, redirect_port, scope):
1616
# Note: This func signature does not and should not require client_secret
1717
fresh_auth_code = AuthCodeReceiver.acquire(
1818
app.get_authorization_request_url(scope), redirect_port)
19-
return app.acquire_token_by_authorization_code(fresh_auth_code, scope)
19+
return app.acquire_token_with_authorization_code(fresh_auth_code, scope)
2020

2121

2222
# Note: This test case requires human interaction to obtain authorization code
@@ -31,7 +31,7 @@ def setUpClass(cls):
3131
cls.config = json.load(open(CONFIG_FILE))
3232
cls.app = ConfidentialClientApplication(
3333
cls.config['CLIENT_ID'], cls.config['CLIENT_SECRET'])
34-
cls.token = acquire_token_by_authorization_code(
34+
cls.token = acquire_token_with_authorization_code(
3535
# Prepare a token. It will be shared among multiple test cases.
3636
cls.app, cls.config.get('REDIRECTION_PORT', 8000), cls.scope2)
3737

@@ -64,7 +64,7 @@ def test_get_authorization_request_url(self):
6464
def beautify(self, json_payload):
6565
return json.dumps(json_payload, indent=2)
6666

67-
def test_acquire_token_by_authorization_code(self):
67+
def test_acquire_token_with_authorization_code(self):
6868
# Actually we already obtain a token during this TestCase initialization
6969
self.assertEqual(self.token.get('error_description'), None)
7070
logging.info("Authorization Code Grant: %s", self.beautify(self.token))

0 commit comments

Comments
 (0)