integrating fusionauth and django-rest-framework

172 views Asked by At

I am trying to integrate FusionAuth and Django-Rest-Framework (with a React frontend), and am getting very confused.

I have some code that kind of works. It uses the "authorization code grant". The React frontend redirects to the FusionAuth login page which once submitted redirects back to the frontend with an authorization_code as a URL Parameter. The frontend passes that code to the Django backend which exchanges it for an access_token. That access_token is used to get some user information from FusionAuth including a unique id with which to create a local Django User (if one doesn't already exist). It then generates a local token and passes that to the frontend to use for authentication in future requests.

Here is some pseudo-code:


from fusionauth.fusionauth_client import FusionAuthClient

client = FusionAuthClient(FA_API_KEY, FA_URL)

def authenticate(request):
  authorization_code = request.data["code"]
  fa_token_response = client.exchange_o_auth_code_for_access_token()
  fa_user_response = client.retrieve_user(user_id=fa_token_response["userId"])
  user, created = UserModel.objects.get_or_create(
      fa_id=fa_token_response["userId"],
      defaults={
        "username": fa_user_response["username"],
        "email": fa_user_response["email"],
      },
  )

  token = generate_token(user)  # THIS IS PROBABLY WRONG

  return Response(
    {
      "token": token,
      "user_id": user.id,
    }
    status=status.HTTP_200_OK,
  )

As you can see, I generate my own token (I happen to be using knox, but that's not important). But I want to just use the same access_token provided by FusionAuth - because that means it will have the same expiry and refresh_token and generally just make life easier.

But I'm not sure how to do that; How to either just re-use the exact same access & refresh tokens, or else write some DRF authentication backend that checks the token against FusionAuth on each request (although that sounds inefficient), or else use some 3rd party library that has already solved this problem.

Any hints?

1

There are 1 answers

0
trubliphone On

I wound up writing a custom DRF Authentication Class which checks the token against FusionAuth:

from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework.authentication import (
    HTTP_HEADER_ENCODING,
    BaseAuthentication,
    get_authorization_header,
)
from rest_framework.exceptions import AuthenticationFailed
from fusionauth.fusionauth_client import FusionAuthClient

AUTH_CLIENT = FusionAuthClient(
    settings.FUSIONAUTH_API_KEY, settings.FUSIONAUTH_URL
)

UserModel = get_user_model()


class OAuth2Authentication(BaseAuthentication):
    """
    Class for performing DRF Authentication using OAuth2 via FusionAuth
    """
    def authenticate(self, request):
        """
        Authenticate the request and return a tuple of (user, token) or None
        if there was no authentication attempt.
        """
        access_token = self.get_access_token(request)
        if not access_token:
            return None

        auth_jwt_response = AUTH_CLIENT.validate_jwt(access_token)
        if not auth_jwt_response.was_successful():
            raise AuthenticationFailed(auth_jwt_response.error_response)
        auth_jwt = auth_jwt_response.success_response["jwt"]
        auth_id = auth_jwt["sub"]

        try:
            user = UserModel.objects.active().get(auth_id=auth_id)
        except UserModel.DoesNotExist as e:
            msg = "User does not exist"
            raise AuthenticationFailed(msg) from e

        return user, access_token

    def get_access_token(self, request):
        """
        Get the access token based on a request.

        Returns None if no authentication details were provided. Raises
        AuthenticationFailed if the token is incorrect.
        """
        header = get_authorization_header(request)
        if not header:
            return None
        header = header.decode(HTTP_HEADER_ENCODING)

        auth = header.split()

        if auth[0].lower() != "bearer":
            return None

        if len(auth) == 1:
            msg = "Invalid 'bearer' header: No credentials provided."
            raise AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = "Invalid 'bearer' header: Credentials string should not contain spaces."
            raise AuthenticationFailed(msg)

        return auth[1]