Skip to content

Commit 834867d

Browse files
author
Massimiliano Pippi
committed
provide DOT policy for client authentication
1 parent 0c85536 commit 834867d

File tree

1 file changed

+89
-29
lines changed

1 file changed

+89
-29
lines changed

oauth2_provider/oauth2_validators.py

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,35 @@
2020
'authorization_code': (Application.GRANT_ALLINONE, Application.GRANT_AUTHORIZATION_CODE),
2121
'password': (Application.GRANT_ALLINONE, Application.GRANT_PASSWORD),
2222
'client_credentials': (Application.GRANT_ALLINONE, Application.GRANT_CLIENT_CREDENTIALS),
23-
'refresh_token': (Application.GRANT_ALLINONE, Application.GRANT_AUTHORIZATION_CODE, Application.GRANT_PASSWORD,
24-
Application.GRANT_CLIENT_CREDENTIALS)
23+
'refresh_token': (Application.GRANT_ALLINONE, Application.GRANT_AUTHORIZATION_CODE,
24+
Application.GRANT_PASSWORD, Application.GRANT_CLIENT_CREDENTIALS)
2525
}
2626

2727

2828
class OAuth2Validator(RequestValidator):
29-
def _authenticate_basic_auth(self, request):
29+
def _extract_basic_auth(self, request):
3030
"""
31-
Authenticates with HTTP Basic Auth
31+
Return authentication string if request contains basic auth credentials, else return None
3232
"""
3333
auth = request.headers.get('HTTP_AUTHORIZATION', None)
34-
3534
if not auth:
36-
return False
35+
return None
3736

3837
auth_type, auth_string = auth.split(' ')
3938
if auth_type != "Basic":
39+
return None
40+
41+
return auth_string
42+
43+
def _authenticate_basic_auth(self, request):
44+
"""
45+
Authenticates with HTTP Basic Auth.
46+
47+
Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with
48+
"application/x-www-form-urlencoded" encoding algorithm.
49+
"""
50+
auth_string = self._extract_basic_auth(request)
51+
if not auth_string:
4052
return False
4153

4254
encoding = request.encoding or 'utf-8'
@@ -45,38 +57,83 @@ def _authenticate_basic_auth(self, request):
4557
client_id, client_secret = auth_string_decoded.split(':', 1)
4658

4759
try:
48-
request.client = Application.objects.get(client_id=unquote_plus(client_id), client_secret=unquote_plus(client_secret))
60+
request.client = Application.objects.get(client_id=unquote_plus(client_id),
61+
client_secret=unquote_plus(client_secret))
4962
return True
5063

5164
except Application.DoesNotExist:
65+
log.debug("Failed basic auth: Application %s do not exists" % unquote_plus(client_id))
5266
return False
5367

5468
def _authenticate_request_body(self, request):
5569
"""
5670
Try to authenticate the client using client_id and client_secret parameters
5771
included in body.
5872
59-
Remember that this method is NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize
60-
the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details.
73+
Remember that this method is NOT RECOMMENDED and SHOULD be limited to clients unable to
74+
directly utilize the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details.
6175
"""
6276
client_id = request.client_id
6377
client_secret = request.client_secret
6478

65-
if not client_id:
79+
if not client_id or not client_secret:
6680
return False
6781

6882
try:
69-
request.client = Application.objects.get(client_id=client_id, client_secret=client_secret)
83+
request.client = Application.objects.get(client_id=client_id,
84+
client_secret=client_secret)
7085
return True
7186

7287
except Application.DoesNotExist:
88+
log.debug("Failed body authentication: Application %s do not exists" % client_id)
7389
return False
7490

91+
def _load_application(self, client_id, request):
92+
"""
93+
If request.client was not set, load application instance for given client_id and store it
94+
in request.client
95+
"""
96+
try:
97+
request.client = request.client or Application.objects.get(client_id=client_id)
98+
except Application.DoesNotExist:
99+
log.debug("Application %s do not exists" % client_id)
100+
101+
def client_authentication_required(self, request, *args, **kwargs):
102+
"""
103+
Determine if the client has to be authenticated
104+
105+
This method is called only for grant types that supports client authentication:
106+
* Authorization code grant
107+
* Resource owner password grant
108+
* Refresh token grant
109+
110+
If the request contains authorization headers, always authenticate the client no matter
111+
the grant type.
112+
113+
If the request does not contain authorization headers, proceed with authentication only if
114+
the client is of type `Confidential`.
115+
116+
If something goes wrong, call oauthlib implementation of the method.
117+
"""
118+
if self._extract_basic_auth(request):
119+
return True
120+
121+
if request.client_id and request.client_secret:
122+
return True
123+
124+
self._load_application(request.client_id, request)
125+
if request.client:
126+
return request.client.client_type == Application.CLIENT_CONFIDENTIAL
127+
128+
return super(OAuth2Validator, self).client_authentication_required(request,
129+
*args, **kwargs)
130+
75131
def authenticate_client(self, request, *args, **kwargs):
76132
"""
77133
Check if client exists and it's authenticating itself as in rfc:`3.2.1`
78134
79-
First we try to authenticate with HTTP Basic Auth, and that is the PREFERRED authentication method.
135+
First we try to authenticate with HTTP Basic Auth, and that is the PREFERRED
136+
authentication method.
80137
Whether this fails we support including the client credentials in the request-body, but
81138
this method is NOT RECOMMENDED and SHOULD be limited to clients unable to directly utilize
82139
the HTTP Basic authentication scheme. See rfc:`2.3.1` for more details
@@ -90,8 +147,9 @@ def authenticate_client(self, request, *args, **kwargs):
90147

91148
def authenticate_client_id(self, client_id, request, *args, **kwargs):
92149
"""
93-
If we are here, the client did not authenticate itself as in rfc:`3.2.1` and we can proceed only if the client
94-
exists and it's not of type 'Confidential'. Also assign Application instance to request.client.
150+
If we are here, the client did not authenticate itself as in rfc:`3.2.1` and we can
151+
proceed only if the client exists and it's not of type 'Confidential'.
152+
Also assign Application instance to request.client.
95153
"""
96154

97155
try:
@@ -119,14 +177,11 @@ def invalidate_authorization_code(self, client_id, code, request, *args, **kwarg
119177

120178
def validate_client_id(self, client_id, request, *args, **kwargs):
121179
"""
122-
Ensure an Application exists with given client_id. Also assign Application instance to request.client.
180+
Ensure an Application exists with given client_id. If it exists, it's assigned to
181+
request.client.
123182
"""
124-
try:
125-
request.client = request.client or Application.objects.get(client_id=client_id)
126-
return True
127-
128-
except Application.DoesNotExist:
129-
return False
183+
self._load_application(client_id, request)
184+
return request.client is not None
130185

131186
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
132187
return request.client.default_redirect_uri
@@ -139,7 +194,8 @@ def validate_bearer_token(self, token, scopes, request):
139194
return False
140195

141196
try:
142-
access_token = AccessToken.objects.select_related("application", "user").get(token=token)
197+
access_token = AccessToken.objects.select_related("application", "user").get(
198+
token=token)
143199
if access_token.is_valid(scopes):
144200
request.client = access_token.application
145201
request.user = access_token.user
@@ -173,8 +229,8 @@ def validate_grant_type(self, client_id, grant_type, client, request, *args, **k
173229

174230
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
175231
"""
176-
We currently do not support the Authorization Endpoint Response Types registry as in rfc:`8.4`, so validate
177-
the response_type only if it matches 'code' or 'token'
232+
We currently do not support the Authorization Endpoint Response Types registry as in
233+
rfc:`8.4`, so validate the response_type only if it matches 'code' or 'token'
178234
"""
179235
if response_type == 'code':
180236
return client.authorization_grant_type == Application.GRANT_AUTHORIZATION_CODE
@@ -196,14 +252,17 @@ def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwarg
196252
return request.client.redirect_uri_allowed(redirect_uri)
197253

198254
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
199-
expires = timezone.now() + timedelta(seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
200-
g = Grant(application=request.client, user=request.user, code=code['code'], expires=expires,
201-
redirect_uri=request.redirect_uri, scope=' '.join(request.scopes))
255+
expires = timezone.now() + timedelta(
256+
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
257+
g = Grant(application=request.client, user=request.user, code=code['code'],
258+
expires=expires, redirect_uri=request.redirect_uri,
259+
scope=' '.join(request.scopes))
202260
g.save()
203261

204262
def save_bearer_token(self, token, request, *args, **kwargs):
205263
"""
206-
Save access and refresh token, If refresh token is issued, remove old refresh tokens as in rfc:`6`
264+
Save access and refresh token, If refresh token is issued, remove old refresh tokens as
265+
in rfc:`6`
207266
"""
208267
if request.refresh_token:
209268
# remove used refresh token
@@ -247,7 +306,8 @@ def validate_user(self, username, password, client, request, *args, **kwargs):
247306
return False
248307

249308
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
250-
# TODO: since this method is invoked *after* validate_refresh_token, could we avoid this second query for RefreshToken?
309+
# TODO: since this method is invoked *after* validate_refresh_token, could we avoid this
310+
# second query for RefreshToken?
251311
rt = RefreshToken.objects.get(token=refresh_token)
252312
return rt.access_token.scope
253313

0 commit comments

Comments
 (0)