I am adding OAuth 2.0 to a new Django-DRF API via Auth0 using Authlib. Everything has always worked fine using a function-based views however when I try to apply the authlib ResourceProtector decorator to a class-based view it keeps returning an error 'ViewSet' object has no attribute 'build_absolute_uri'.
How can I use the Authlib resource protector decorator to add OAuth to a class-based view?
Views.py
from api.permissions import auth0_validator
from authlib.integrations.django_oauth2 import ResourceProtector
from django.http import JsonResponse
require_oauth = ResourceProtector()
validator = auth0_validator.Auth0JWTBearerTokenValidator(
os.environ['AUTH0_DOMAIN'],
os.environ['AUTH0_IDENTIFIER']
)
require_oauth.register_token_validator(validator)
#Resource protector decorator works here
@require_oauth()
def index(request):
return Response('Access granted')
class Users(ModelViewSet):
#Resource protector decorator does not work and invokes error below
@require_oauth()
def list(self, request):
return Response('access granted')
stack trace
Internal Server Error: /v2/statistics
Traceback (most recent call last):
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/sentry_sdk/integrations/django/views.py", line 68, in sentry_wrapped_callback
return callback(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/authlib/integrations/django_oauth2/resource_protector.py", line 39, in decorated
token = self.acquire_token(request, scopes)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/authlib/integrations/django_oauth2/resource_protector.py", line 25, in acquire_token
url = request.build_absolute_uri()
AttributeError: 'StatisticsViewSet' object has no attribute 'build_absolute_uri'
Internal Server Error: /v2/statistics
Traceback (most recent call last):
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/sentry_sdk/integrations/django/views.py", line 68, in sentry_wrapped_callback
return callback(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/authlib/integrations/django_oauth2/resource_protector.py", line 39, in decorated
token = self.acquire_token(request, scopes)
File "/Users/td/Desktop/test-api/lib/python3.8/site-packages/authlib/integrations/django_oauth2/resource_protector.py", line 25, in acquire_token
url = request.build_absolute_uri()
AttributeError: 'StatisticsViewSet' object has no attribute 'build_absolute_uri'
After digging through Authlib, it turns out its Django integration doesn't support class based views. This is because the first parameter in the ResourceProtectors decorator function, will be the view object instead of the request since it's being called on a class method. To fix this I simply extended the ResourceProtector class and added an extra 'view' parameter so that it can be applied to class methods.
class CustomResourceProtector(ResourceProtector):
def __call__(self, scopes=None, optional=False):
def wrapper(f):
@functools.wraps(f)
def decorated(view, request, *args, **kwargs): #Added view as the first argument so it works with class based view methods
try:
token = self.acquire_token(request, scopes)
request.oauth_token = token
except MissingAuthorizationError as error:
if optional:
request.oauth_token = None
return f(request, *args, **kwargs)
return return_error_response(error)
except OAuth2Error as error:
return return_error_response(error)
return f(request, *args, **kwargs)
return decorated
return wrapper
To make it even more python and prevent having to decorate every single method. I turned the decorator into a DRF permission class by further extending the ResourceProtector class to make it return a boolean instead of a decorator
permissions.py
from auth0 import CustomResourceProtector
class OAuthPermission(permissions.BasePermission):
"""
Ensures request has a valid OAuth token to access the endpoint.
"""
message = 'Permission denied, invalid access token.'
def has_permission(self, request, view):
oauth_protector = CustomResourceProtector()
validator = Auth0JWTBearerTokenValidator(
os.environ['AUTH0_DOMAIN'],
os.environ['AUTH0_IDENTIFIER']
)
oauth_protector.register_token_validator(validator)
if oauth_protector.is_token_valid(request):
return True
return False
auth0.py
import os
import json
import functools
from django.http import JsonResponse
from rest_framework import permissions
from authlib.integrations.django_oauth2 import ResourceProtector
from authlib.oauth2.rfc6749.errors import *
from urllib.request import urlopen
from authlib.oauth2.rfc7523 import JWTBearerTokenValidator
from authlib.jose.rfc7517.jwk import JsonWebKey
class CustomResourceProtector(ResourceProtector):
def is_token_valid(self, request):
try:
scopes = None
token = self.acquire_token(request, scopes)
#request.oauth_token = token
return token
except Exception as e:
return False
#Auth0 Authlib token validator - validates Auth0 access tokens
class Auth0JWTBearerTokenValidator(JWTBearerTokenValidator):
def __init__(self, domain, audience):
issuer = f"https://{domain}/"
jsonurl = urlopen(f"{issuer}.well-known/jwks.json")
public_key = JsonWebKey.import_key_set(
json.loads(jsonurl.read())
)
super(Auth0JWTBearerTokenValidator, self).__init__(
public_key
)
self.claims_options = {
"exp": {"essential": True},
"aud": {"essential": True, "value": audience},
"iss": {"essential": True, "value": issuer},
}