diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 2fc7061..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/src/canvas_oauth/admin.py b/src/canvas_oauth/admin.py new file mode 100644 index 0000000..e45595b --- /dev/null +++ b/src/canvas_oauth/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from canvas_oauth.models import CanvasOAuth2Token + + +@admin.register(CanvasOAuth2Token) +class CanvasOAuth2TokenAdmin(admin.ModelAdmin): + list_display = ["user", "created_on", "updated_on", "expires"] + search_fields = ["user__username", "user__email"] + # Tokens only come from the OAuth flow; the admin is for viewing and deleting + exclude = ["access_token", "refresh_token"] + readonly_fields = ["user", "expires", "created_on", "updated_on"] + + def has_add_permission(self, request): + return False diff --git a/src/canvas_oauth/canvas.py b/src/canvas_oauth/canvas.py index 466751d..975c845 100755 --- a/src/canvas_oauth/canvas.py +++ b/src/canvas_oauth/canvas.py @@ -4,7 +4,7 @@ import requests from django.utils import timezone -from canvas_oauth.exceptions import InvalidOAuthReturnError +from canvas_oauth.exceptions import InvalidOAuthReturnError, InvalidGrantError from canvas_oauth import settings logger = logging.getLogger(__name__) @@ -85,6 +85,8 @@ def get_access_token( r = requests.post(oauth_token_url, post_params) logger.info("%s POST response from Canvas is %s", grant_type, r.text) if r.status_code != 200: + if r.status_code in (400, 401) and _error_code(r) == "invalid_grant": + raise InvalidGrantError("%s grant was rejected: %s" % (grant_type, r.text)) raise InvalidOAuthReturnError( "%s request failed to get a token: %s" % (grant_type, r.text) ) @@ -104,3 +106,10 @@ def get_access_token( refresh_token = response_data["refresh_token"] return (access_token, expires, refresh_token) + + +def _error_code(response): + try: + return response.json().get("error") + except ValueError: + return None diff --git a/src/canvas_oauth/exceptions.py b/src/canvas_oauth/exceptions.py index a5fecd9..ad572cf 100755 --- a/src/canvas_oauth/exceptions.py +++ b/src/canvas_oauth/exceptions.py @@ -12,3 +12,9 @@ class InvalidOAuthStateError(CanvasOAuthError): class InvalidOAuthReturnError(CanvasOAuthError): pass + + +class InvalidGrantError(InvalidOAuthReturnError): + """Canvas no longer recognizes the grant (e.g. the refresh token was revoked)""" + + pass diff --git a/src/canvas_oauth/oauth.py b/src/canvas_oauth/oauth.py index a634af6..c8b75ce 100755 --- a/src/canvas_oauth/oauth.py +++ b/src/canvas_oauth/oauth.py @@ -11,7 +11,11 @@ from canvas_oauth import canvas, settings from canvas_oauth.models import CanvasOAuth2Token -from canvas_oauth.exceptions import MissingTokenError, InvalidOAuthStateError +from canvas_oauth.exceptions import ( + MissingTokenError, + InvalidOAuthStateError, + InvalidGrantError, +) logger = logging.getLogger(__name__) @@ -96,11 +100,13 @@ def oauth_callback(request): code=code, ) - CanvasOAuth2Token.objects.create( + CanvasOAuth2Token.objects.update_or_create( user=request.user, - access_token=access_token, - expires=expires, - refresh_token=refresh_token, + defaults={ + "access_token": access_token, + "expires": expires, + "refresh_token": refresh_token, + }, ) return redirect(request.session["canvas_oauth_initial_uri"]) @@ -134,13 +140,20 @@ def refresh_oauth_token(request): # Get the new access token and expiration date via # a refresh token grant - oauth_token.access_token, oauth_token.expires, _ = canvas.get_access_token( - grant_type="refresh_token", - client_id=settings.CANVAS_OAUTH_CLIENT_ID, - client_secret=settings.CANVAS_OAUTH_CLIENT_SECRET, - redirect_uri=request.build_absolute_uri(reverse("canvas-oauth-callback")), - refresh_token=oauth_token.refresh_token, - ) + try: + oauth_token.access_token, oauth_token.expires, _ = canvas.get_access_token( + grant_type="refresh_token", + client_id=settings.CANVAS_OAUTH_CLIENT_ID, + client_secret=settings.CANVAS_OAUTH_CLIENT_SECRET, + redirect_uri=request.build_absolute_uri(reverse("canvas-oauth-callback")), + refresh_token=oauth_token.refresh_token, + ) + except InvalidGrantError: + # Canvas no longer recognizes the refresh token (revoked, key reset, etc.), + # so drop it and send the user back through authorization + logger.warning("Refresh token rejected for user %s", request.user.pk) + oauth_token.delete() + raise MissingTokenError("Refresh token rejected for user %s" % request.user.pk) # Update the model with new token and expiration oauth_token.save()