DEV Community

Cover image for Implementing Authorization in FastAPI: A Step-by-Step Guide to Securing Your Web Applications
Osazuwa J. Agbonze
Osazuwa J. Agbonze

Posted on • Updated on

Implementing Authorization in FastAPI: A Step-by-Step Guide to Securing Your Web Applications

In today's world, building secure and reliable web applications is more important than ever. One crucial aspect of this is implementing proper authorization protocols to protect sensitive user data and ensure that only authorized users can access certain resources.

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. One of the key advantages of FastAPI is its built-in support for handling user authentication and authorization.

FastAPI has built-in support for handling authentication through the use of JSON Web Tokens (JWT). Once a user is authenticated, their JWT can be used to authorize them to access certain resources or perform certain actions. In this guide, we'll see how to do just that. Below is an outline on what we'll touch on in this guide.

  1. FastAPI authentication scheme setup
  2. Data access protection configuration
  3. Access authorization

To follow along, do note:

This guide will build on JWT authentication in FastAPI. Yet to see the guide? then check it out, i'll wait here. Github repository is also available, so you can clone the project and begin from where we left off

Generating JWT token in FastAPI for authentication

FastAPI authentication scheme setup

To access core features in most applications, logging in is usually required. While some application would only provide an option to log in with email and password (this is what we'll use in this guide), others might require that and also include log in with google, facebook you get the gist. These options have a distinct interface which collects user credentials and a process (view, controller, service e.t.c) that validates the credentials. All of these together is an authentication scheme: Authentication scheme defines what's needed for an authentication process, these include:

  • the kind of authentication to be supported e.g auth with email & password, social auth e.t.c
  • an interface to collect auth credentials e.g SPA app / postman e.t.c
  • a process to validate the credentials and return a token

authentication scheme with different login options to sign in to spaceofmiah


Authentication Scheme with different login options

When it comes to setting up authentication scheme, FastAPI shines with it's setup simplicity.Open main.py and include the following content



# other imports statement at the top
from fastapi.security import OAuth2PasswordBearer

# setup authentication scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")


Enter fullscreen mode Exit fullscreen mode

The above code is an authentication scheme which would request that users provide a username and password to login. We imported and instantiated 0Auth2PasswordBearer with a route to handle login process fed to tokenUrl param. The login route would generate a JWT token on successful authentication.

Data access protection configuration

Data privacy is an important topic which should be taken very seriously. You only want to grant data access to authorized users. We'll see how to facilitate data privacy in FastAPI by allowing only authorized access to user's profile. Open up main.py and include the following code



@app.get("/profile/{id}", response_model=UserSchema)
def profile(id:int, session:Session=Depends(get_db)):
    """Processes request to retrieve user
    profile by id
    """
    return user_db_services.get_user_by_id(session=session, id=id)


Enter fullscreen mode Exit fullscreen mode

We've defined a profile view which is accessible on route /profile/{id}. It takes as a path parameter the user's id whose profile is to be viewed. Response type is a UserSchema. The body of the view feeds the id and a database session to get_user_by_id service. This service is currently unavailable, open up services/db/users.py to include it:



# other functions and import statements above

def get_user_by_id(session:Session, id:int):
    return session.query(User).filter(User.id == id).one()


Enter fullscreen mode Exit fullscreen mode

get_user_by_id function queries the User database table and filter user having an id value similar to that retrieved from path parameter from our profile view.

At this point we've not set any data restrictions (we'll get to this shortly and you'd see how easy FastAPI has made the setup) meaning all user's profile data is accessible to the general public. Lets test this out. Go to project root and run docker-compose up --build -d --no-deps


Note
You might want to run migrations to avoid database interaction errors. Alembic is smart enough and wouldn't make any migration that has already been made, so it's fine if you re-run the migration.


In your browser, open localhost:8000/docs and you should be presented with this view

Build with FastAPI - docs route snipshot of profile view

Build with FastAPI - docs route snipshot of profile view

Create a new user by interacting with the signup route by using the try it out button. When a user is created, the response would return the new user's id which will be needed to interact with the profile view. Below is an image of what the response should look like

Build with FastAPI - create new user response

Build with FastAPI - create new user response

In my case, the new user id is 2. we'll proceed to interacting with the profile endpoint with the new user's id. Click on try it out on the profile endpoint and you should have a form field where an id is requested for.

Build with FastAPI - user profile route with empty id field

Build with FastAPI - user profile route

On clicking execute, you should have a response similar to the one gotten on creation of the user.


To set data privacy, we'll utilize the authentication scheme setup earlier by supplying it as a parameter to profile view signature.

main.py -- updating profile view



@app.get("/profile/{id}", response_model=UserSchema)
def profile(
    id:int, 
    token: str = Depends(oauth2_scheme),
    session:Session=Depends(get_db)
):
        """Processes request to retrieve the requesting user
    profile 
    """
    return user_db_services.get_user_by_id(session=session, id=id)


Enter fullscreen mode Exit fullscreen mode

With that simple addition, we should now have an Authorize button on the docs page as shown below


Can't find expected update on the Docs ?

If you can't find the changes on the docs page or your changes not working as expected, restarting your app container service might fix the issue

docker-compose restart app

Didn't work ? leave a comment below


Build with FastAPI - authorize button

Build with FastAPI - authorize button

Attempt to authorize a user by clicking on any of the authorize buttons would result to 422 Unprocessable Entity Error. Clicking on try it out on the profile endpoint that previously returned the user detail whose id was passed should now return Unauthorized as seen in the image below

Build with FastAPI - unauthorized access

Build with FastAPI - unauthorized access

Fixing Unprocessable Entity Error

Our login view is currently using UserLoginSchema to capture authentication details. While this is not entirely wrong, the token provided as response from the endpoint cannot be used to interact with endpoints that requires authorization on the Docs. So lets fix this

main.py



from fastapi.security import OAuth2PasswordRequestForm

@app.post('/login', response_model=Dict)
def login(
        payload: OAuth2PasswordRequestForm = Depends(),
        session: Session = Depends(get_db)
    ):
    """Processes user's authentication and returns a token
    on successful authentication.

    request body:

    - username: Unique identifier for a user e.g email, 
                phone number, name

    - password:
    """
    try:
        user:user_model.User = user_db_services.get_user(
            session=session, email=payload.username
        )
    except:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid user credentials"
        )

    is_validated:bool = user.validate_password(payload.password)
    if not is_validated:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid user credentials"
        )

    return user.generate_token()


Enter fullscreen mode Exit fullscreen mode

We imported OAuth2PasswordRequestForm and utilized it in login function signature as a type to payload parameter which is automatically injected by Depends. Changes was also made on user retrieval from database by using payload.username (initially was payload.email ). Those are all the needed changes.

I'm building a simple API to test a database. When I use GET request everything works fine, but if I change to POST, I get 422 Unprocessable Entity error.

Here is the FastAPI code:

from fastapi import FastAPI
app = FastAPI()

@app.post("/")
def main(user):
    return user

Then, my request…

Access authorization

To gain authorized access first click on the authorize button which should present a form with username and password field. Provide the email of the user created earlier as username and the password to authenticate. On successful authentication, clicking on the authorize button again should show that authorization access has been granted

Build with FastAPI - successful authorization

Build with FastAPI - successful authorization

With authorized access now granted, interacting with the endpoint to retrieve a user's profile would work fine.

Summary

In this guide we looked at authorization scheme and what it's comprised of. We also touched on data privacy and how easy FastAPI has made it's implementation.


Thanks for taking the time to read through. With the ability to setup a JWT authentication and authorization scheme in FastAPI, you can further extend what you've learnt and include it in your project. I'm currently using this concept to restrict unauthorized interaction with an object storage.

Code is on Github

Top comments (2)

Collapse
 
giuseppericci profile image
giuseppericci

Hi Osazuwa, can you suggest a method to retrieve the current user and protect an endpoint withouth the user id which it can retrieved with a get_current_user function??
Thanks for your support.

Collapse
 
spaceofmiah profile image
Osazuwa J. Agbonze • Edited

One way i've achieved this is to use jwt token (this post uses OAuth2PasswordBearer for auth) with fastapi Depends callable which takes a function that can decode a jwt token e.g

@router.get(
    "/profile",
    status_code=status.HTTP_200_OK,
)
def profile(account=Depends(auth_account)) -> AccountResponse:
    """Get authenticated account"""
    return account
Enter fullscreen mode Exit fullscreen mode

auth_account is a function that can decode a jwt token from which the authenticated user on the request can be gotten.

I will publish another post on this explaining each details soon