-
Notifications
You must be signed in to change notification settings - Fork 23
Added support for client_credentials grant type #25
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| __version__ = '0.4.0' | ||
| __version__ = '0.5.0' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |
| from provider import constants | ||
| from provider.oauth2.backends import BasicClientBackend, RequestParamsClientBackend, PublicPasswordBackend | ||
| from provider.oauth2.forms import (AuthorizationCodeGrantForm, AuthorizationRequestForm, AuthorizationForm, | ||
| PasswordGrantForm, RefreshTokenGrantForm) | ||
| PasswordGrantForm, RefreshTokenGrantForm, ClientCredentialsGrantForm) | ||
| from provider.oauth2.models import Client, RefreshToken, AccessToken | ||
| from provider.utils import now | ||
| from provider.views import AccessToken as AccessTokenView, OAuthError, AccessTokenMixin, Capture, Authorize, Redirect | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This (i.e., I understand wanting to be as surgical as possible when it comes to touching this codebase. However, since you've fixed the imports here, would you be opposed to also fixing this so that
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. I'm not touching any of that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An easy fix would be to import views and use views.AccessToken, etc. Or you could not touch it.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll take "not touching the code we want to kill" for 400, please, Alex. I seriously don't even want to make this PR. I would much rather invest the time/energy in getting us to django-oauth-toolkit. This works. It's tested. I'm not touching it. If anyone feels strongly about renaming, submit another PR. |
||
|
|
@@ -24,7 +24,6 @@ def get_access_token(self, request, user, scope, client): | |
| except AccessToken.DoesNotExist: | ||
| # None found... make a new one! | ||
| at = self.create_access_token(request, user, scope, client) | ||
| self.create_refresh_token(request, user, scope, at, client) | ||
| return at | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change makes sense since not all grant types should create a refresh token. In fact, looking at the code now, there's an unintentional bug with Public clients using the Password Grant with constants.SINGLE_ACCESS_TOKEN enabled - the code generated refresh tokens for them. My suggestion to you is to use the recommendation in the Wrong Abstraction article. Have each of the 3 callers of the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we are making 2 assumptions here:
Not knowing what future lies ahead of us, in the meantime, why have broken functionality in this common library? My concern is that (A) current (if any open edX instance has enabled constants.SINGLE_ACCESS_TOKEN) and (B) future uses of this library would not be doing the correct thing. (By the way, the Mobile team was, in fact, considering to enable constants.SINGLE_ACCESS_TOKEN on edx.org since the access control table continues to grow in the multi-device and multi-session use cases.) So either: (1) remove support for constants.SINGLE_ACCESS_TOKEN entirely - with notification to edx-code, (2) pass in a boolean to this method on whether or not a refresh_token is expected, or (3) refactor the abstraction as suggested above. I believe, 2 or 3 should be reasonable to implement. What do you say?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've implemented a combination of 2 and 3. Please have a look at |
||
|
|
||
| def create_access_token(self, request, user, scope, client): | ||
|
|
@@ -140,6 +139,12 @@ def get_password_grant(self, request, data, client): | |
| raise OAuthError(form.errors) | ||
| return form.cleaned_data | ||
|
|
||
| def get_client_credentials_grant(self, request, data, client): | ||
| form = ClientCredentialsGrantForm(data, client=client) | ||
| if not form.is_valid(): | ||
| raise OAuthError(form.errors) | ||
| return form.cleaned_data | ||
|
|
||
| def invalidate_grant(self, grant): | ||
| if constants.DELETE_EXPIRED: | ||
| grant.delete() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,25 @@ def access_token_response_data(self, access_token, response_type=None): | |
|
|
||
| return response_data | ||
|
|
||
| def get_access_and_refresh_tokens(self, request, user, scope, client, reuse_existing_access_token=False, create_refresh_token=True): | ||
| """ | ||
| Returns an AccessToken and RefreshToken for the given user, scope, and client combination. | ||
|
|
||
| Returns: | ||
| (AccessToken, RefreshToken) | ||
| If create_refresh_token is False, the second element of the tuple will be None. | ||
| """ | ||
| if reuse_existing_access_token: | ||
| at = self.get_access_token(request, user, scope, client) | ||
| else: | ||
| at = self.create_access_token(request, user, scope, client) | ||
|
|
||
| rt = None | ||
| if create_refresh_token and not reuse_existing_access_token: | ||
| rt = self.create_refresh_token(request, user, scope, at, client) | ||
|
|
||
| return at, rt | ||
|
|
||
|
|
||
| class OAuthView(TemplateView): | ||
| """ | ||
|
|
@@ -333,15 +352,14 @@ def get_implicit_response(self, request, client): | |
| data = self.get_data(request) | ||
|
|
||
| lookup_kwargs = { | ||
| "user": request.user, | ||
| "client": client, | ||
| "scope": scope.to_int(*data.get('scope', constants.SCOPES[0][1]).split()) | ||
| 'user': request.user, | ||
| 'client': client, | ||
| 'scope': scope.to_int(*data.get('scope', constants.SCOPES[0][1]).split()), | ||
| 'reuse_existing_access_token': constants.SINGLE_ACCESS_TOKEN, | ||
| 'create_refresh_token': False | ||
| } | ||
|
|
||
| if constants.SINGLE_ACCESS_TOKEN: | ||
| token = self.get_access_token(request, **lookup_kwargs) | ||
| else: | ||
| token = self.create_access_token(request, **lookup_kwargs) | ||
| token, __ = self.get_access_and_refresh_tokens(request, **lookup_kwargs) | ||
|
|
||
| response_data = self.access_token_response_data(token, data['response_type']) | ||
|
|
||
|
|
@@ -503,7 +521,7 @@ class AccessToken(OAuthView, Mixin, AccessTokenMixin): | |
| Authentication backends used to authenticate a particular client. | ||
| """ | ||
|
|
||
| grant_types = ['authorization_code', 'refresh_token', 'password'] | ||
| grant_types = ['authorization_code', 'refresh_token', 'password', 'client_credentials'] | ||
| """ | ||
| The default grant types supported by this view. | ||
| """ | ||
|
|
@@ -532,6 +550,14 @@ def get_password_grant(self, request, data, client): | |
| """ | ||
| raise NotImplementedError # pragma: no cover | ||
|
|
||
| def get_client_credentials_grant(self, request, data, client): | ||
| """ | ||
| Return the optional parameters (scope) associated with this request. | ||
|
|
||
| :return: ``tuple`` - ``(True or False, options)`` | ||
| """ | ||
| raise NotImplementedError # pragma: no cover | ||
|
|
||
| def invalidate_grant(self, grant): | ||
| """ | ||
| Override to handle grant invalidation. A grant is invalidated right | ||
|
|
@@ -564,13 +590,16 @@ def authorization_code(self, request, data, client): | |
| Handle ``grant_type=authorization_code`` requests as defined in | ||
| :rfc:`4.1.3`. | ||
| """ | ||
| grant = self.get_authorization_code_grant(request, request.POST, | ||
| client) | ||
| if constants.SINGLE_ACCESS_TOKEN: | ||
| at = self.get_access_token(request, grant.user, grant.scope, client) | ||
| else: | ||
| at = self.create_access_token(request, grant.user, grant.scope, client) | ||
| rt = self.create_refresh_token(request, grant.user, grant.scope, at, client) | ||
| grant = self.get_authorization_code_grant(request, request.POST, client) | ||
|
|
||
| kwargs = { | ||
| 'request': request, | ||
| 'user': grant.user, | ||
| 'scope': grant.scope, | ||
| 'client': client, | ||
| 'reuse_existing_access_token': constants.SINGLE_ACCESS_TOKEN, | ||
| } | ||
| at, rt = self.get_access_and_refresh_tokens(**kwargs) | ||
|
|
||
| self.invalidate_grant(grant) | ||
|
|
||
|
|
@@ -586,8 +615,13 @@ def refresh_token(self, request, data, client): | |
| self.invalidate_refresh_token(rt) | ||
| self.invalidate_access_token(rt.access_token) | ||
|
|
||
| at = self.create_access_token(request, rt.user, rt.access_token.scope, client) | ||
| rt = self.create_refresh_token(request, at.user, at.scope, at, client) | ||
| kwargs = { | ||
| 'request': request, | ||
| 'user': rt.user, | ||
| 'scope': rt.access_token.scope, | ||
| 'client': client, | ||
| } | ||
| at, rt = self.get_access_and_refresh_tokens(**kwargs) | ||
|
|
||
| return self.access_token_response(at) | ||
|
|
||
|
|
@@ -597,16 +631,32 @@ def password(self, request, data, client): | |
| """ | ||
|
|
||
| data = self.get_password_grant(request, data, client) | ||
| user = data.get('user') | ||
| scope = data.get('scope') | ||
| kwargs = { | ||
| 'request': request, | ||
| 'user': data.get('user'), | ||
| 'scope': data.get('scope'), | ||
| 'client': client, | ||
| 'reuse_existing_access_token': constants.SINGLE_ACCESS_TOKEN, | ||
|
|
||
| if constants.SINGLE_ACCESS_TOKEN: | ||
| at = self.get_access_token(request, user, scope, client) | ||
| else: | ||
| at = self.create_access_token(request, user, scope, client) | ||
| # Public clients don't get refresh tokens | ||
| if client.client_type == constants.CONFIDENTIAL: | ||
| rt = self.create_refresh_token(request, user, scope, at, client) | ||
| 'create_refresh_token': client.client_type == constants.CONFIDENTIAL | ||
| } | ||
| at, rt = self.get_access_and_refresh_tokens(**kwargs) | ||
|
|
||
| return self.access_token_response(at) | ||
|
|
||
| def client_credentials(self, request, data, client): | ||
| """ Handle ``grant_type=client_credentials`` requests as defined in :rfc:`4.4`. """ | ||
| data = self.get_client_credentials_grant(request, data, client) | ||
| kwargs = { | ||
| 'request': request, | ||
| 'user': client.user, | ||
| 'scope': data.get('scope'), | ||
| 'client': client, | ||
| 'reuse_existing_access_token': constants.SINGLE_ACCESS_TOKEN, | ||
| 'create_refresh_token': False, | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this also need
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Added. |
||
| at, rt = self.get_access_and_refresh_tokens(**kwargs) | ||
|
|
||
| return self.access_token_response(at) | ||
|
|
||
|
|
@@ -622,6 +672,8 @@ def get_handler(self, grant_type): | |
| return self.refresh_token | ||
| elif grant_type == 'password': | ||
| return self.password | ||
| elif grant_type == 'client_credentials': | ||
| return self.client_credentials | ||
| return None | ||
|
|
||
| def get(self, request): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the success case, verify that:
access_tokenis returned (with anexpires_infield).refresh_tokenis NOT returned.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my latest comment regarding testing for refresh tokens. This code base is...not ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow. Perhaps I missed an earlier conversation on this.
But the RFC specifically states in Section 4.4.3 for the Client Credentials Grant:
So, if the Client Credentials grant is implemented correctly, then we should make sure in the test that we don't return a
refresh_token.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it as simple as adding a line:
self.assertNotIn('refresh_token', response.data)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. This is why I wanted someone intimately familiar with the spec reviewing. This code is being adapted from caffeinehit#47. I admin I haven't fully read the spec, so I was unaware of the need to omit the refresh token. That is great, because testing that has proved frustrating!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. It's an unmerged PR and still has issues.