Top Rated Plus on Upwork with a 100% Job Success ScoreView on Upwork
retzdev logo
logo
tech

API Authorization Using FastAPI, Auth0, and Neo4j

by Jarrett Retz

October 17th, 2023 fastapi python authorization api auth0 neo4j

Introduction

I was surprised to see that it's been over two years since my last blog post on Python or FastAPI. Actually, it's been over two years since my last programming article—period.

Therefore, I had the itch to restart and share another Python post. I have already written two articles on the subject:

This post will have some overlap but, instead, will look at how to use Auth0 to authorize users, and how to use the user identification from Auth0 to query a local Neo4j server.

There are some holes in this approach, so this will not be an all-encompassing post, but I'll point out where it's lacking.

Authorization Sequence Diagram

In our web application setup, a client representing the user authenticates using a username and password to the Auth0 server. Then, the client receives a token, which is subsequently sent to our custom API on each request authorizing access to the data. The sequence diagram below shows this process in the first three steps.

Next, the API needs to decode the token from the client before querying the database. If all goes well, the database returns the user to the API, and the API can send data back to the user.

Let's take a closer look at each step with some Python code.

How to Setup FastAPI Authorization with Auth0

There are a couple of ways the user can retrieve a token.

The latter is not recommended, however, it's what I used to test the authorization process because it allows me to use Postman to retrieve the user token and test the token validity.

# The login route is used for retrieving a user token when testing with
# an API client (i.e. Postman) and is not how the user will normally
# authenticate to use the application.
@router.post(
    "/",
    description=(
        "Login user using Auth0 with Username Password Authentication to get access"
        " token for API."
    ),
    response_model=Auth0UsernamePasswordCredentials,
)
async def login(
    raw_user_credentials: UserAuthentication,
):
    username = raw_user_credentials.username
    password = raw_user_credentials.password
    audience = settings.auth0_audience

    try:
        resp = get_token.login(
            username=username,
            password=password,
            realm="Username-Password-Authentication",
            audience=audience,
        )
        return resp
    except Auth0Error as e:
        # TODO: Log error
        print(e.message)
        raise AuthenticationProviderException

I'll share the code in pieces for this article, but you can take a more comprehensive look if you check out the gist.

This route is triggered with a POST request with the username and password of the user in the body. Then, using the GetToken class in the auth0-python SDK, the API sends the credentials to Auth0. If it works, the API gets an access token which it can send back to the client.

Decoding Auth0 Token with Python

Now that the client has a token, it's authorized to request data from the API. However, instead of returning data, we're going to take the token, decode it, and then use the decoded information to query our database for the user that sent the request. Finally, we'll return that information to the client.

First, let's look at where the request will enter the API in the router.

@router.get(
    "/",
    name="User",
    description="Get the authenticated client user in database.",
    response_model=User,
    responses={401: {"model": HTTPException}},
)
async def root(user: User = Depends(active_user)):
    return user

Pretty simple! But, we need to pull on the dependency string to see where it leads. The login function receives the user argument as a function of the dependency, Depends(active_user).

The active_user function is the first in a series of middleware (dependency) functions that each does its own part in getting us the user data.

def active_user(userInDb: Annotated[UserInDB, Depends(active_user_in_db)]) -> User:
    """Initiates a `User` from a `UserInDB` model"""

    # Remove sensitive information
    user = User(**userInDb)

    return user

active_user places the UserInDB Pydantic data model into the User data model, effectively stripping it of sensitive information.

This function gets the userInDb argument from the active_user_in_db function.

def active_user_in_db(
    decodePayload: Annotated[JWTPayload, Depends(validate_token)]
) -> User:
    """Find user in database matching Auth0 `sub` parameter in JWT Payload"""

    # Create session for a sequence of transactions
    with driver.session() as session:
        # Value to identify user in database
        sub = decodePayload.sub

        try:
            # Commit user query
            userInDb = session.execute_read(get_user, sub=sub)
            return userInDb
        except Neo4jError as e:
            print(e.message)
            raise DatabaseError

active_user_in_db is the step in the process where we take the decoded JWT payload, access the sub parameter (Auth0 user identification), and then match the value to a value in our Neo4j database.

So, where do we get the decoded JWT payload? Continuing our journey back through the API we see the validate_token dependency function providing the decodedPayload argument.

# FastAPI 'Dependency' for parsing Authorization header `Bearer [token]`
token_auth_scheme = HTTPBearer()


def validate_token(
    token: Annotated[HTTPAuthorizationCredentials, Depends(token_auth_scheme)]
) -> JWTPayload:
    """Validates Bearer JWT with Auth0 configuration"""

    payload = JSONWebToken(token.credentials).validate()

    return JWTPayload(**payload)

This function uses a JSONWebToken class to validate the credentials:

JSONWebToken(token.credentials).validate()

The class is built using Auth0 configuration data stored in the settings object. You can look at how to set up environment settings values here.

from dataclasses import dataclass
import jwt
from exceptions.custom_exceptions import (
    BadCredentialsException,
    UnableCredentialsException,
)
from .config import settings


# https://github.com/auth0-developer-hub/api_fastapi_python_hello-world/blob/basic-authorization/application/json_web_token.py
@dataclass
class JSONWebToken:
    """Perform JSON Web Token (JWT) validation using PyJWT"""

    jwt_access_token: str
    auth0_issuer_url: str = f"https://{settings.auth0_domain}/"
    auth0_audience: str = settings.auth0_audience
    algorithm: str = "RS256"
    jwks_uri: str = f"{auth0_issuer_url}.well-known/jwks.json"

    def validate(self):
        try:
            jwks_client = jwt.PyJWKClient(self.jwks_uri)
            jwt_signing_key = jwks_client.get_signing_key_from_jwt(
                self.jwt_access_token
            ).key
            payload = jwt.decode(
                self.jwt_access_token,
                jwt_signing_key,
                algorithms=self.algorithm,
                audience=self.auth0_audience,
                issuer=self.auth0_issuer_url,
            )
        except jwt.exceptions.PyJWKClientError:
            raise UnableCredentialsException
        except jwt.exceptions.InvalidTokenError as e:
            print(e)
            raise BadCredentialsException
        return payload

The code for validating the token is from the Auth0 Developer Hub repository.

If only it were all as simple as copying this code into your API! But it's not, and it wasn't for me. A lot of configuration went into this setup in Auth0 for authorizing applications, grant types, URLs, and username-password access. It was, unfortunately, a headache.

Querying Neo4j for User Data in FastAPI

I skipped over the part where the API queries the Neo4j database using the sub value for the user data. However, that part is included in the Github gist with the rest of the code.

Using Auth0 with Neo4j was born out of the article, Handling Authentication and Identity with Neo4j and Auth0. That said, most of the code is from Neo4j's Python manual. The code in the manual isn't typed, but I was able to find a few argument types, so you can reference my code for that aspect.

Pitfalls

Syncing users. It's not easy to keep the Auth0 user database and your Neo4j database in sync with this setup. You'll have to configure Actions to handle adding new users to the Neo4j database after they sign up with Auth0. Or, Auth0 offers other ways to sync with databases.

Duplicate users. In the article, Handling Authentication and Identity with Neo4j and Auth0, the author discusses the problem of duplicate users. These are users who signed up using different Auth0 methods (i.e., Github, Google, E-mail). He provides a remedy in the article.

Configuration. I ran into a lot of configuration errors. I had weird Python SSL certificate errors, grant type errors, password authorization errors, wrong client ID problems, etc. There's a large part of this article that isn't discussed and that's the Auth0 setup on their web platform. For navigating that subject you'll have to refer to Auth0 documentation and community forums.

Jarrett Retz

Jarrett Retz is a freelance web application developer and blogger based out of Spokane, WA.

jarrett@retz.dev

Subscribe to get instant updates

Contact

jarrett@retz.dev

Legal

Any code contained in the articles on this site is released under the MIT license. Copyright 2024. Jarrett Retz Tech Services L.L.C. All Rights Reserved.