Python Authlib: 'View' object has no attribute 'get_absolute_uri'

Question:

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'
Asked By: Tamdim

||

Answers:

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},
        }
Answered By: Tamdim
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.