Python API Development with FastAPI

Advanced FastAPI

Protecting Routes

In this article, we complete the authentication setup for our FastAPI application by enforcing that users must be authenticated before creating, deleting, or updating posts. Without this protection, any client could call our endpoints and perform operations without logging in.

For example, the following JSON payload demonstrates how a post can be created without any authentication:

{
    "title": "top beaches in florida",
    "content": "something somethng beaches",
    "rating": 4
}

Without user authentication, anyone can create, delete, or modify posts. Depending on your application's design—for instance, a Twitter-like platform where everyone can view tweets but only the owner can modify them—you might want to restrict certain operations to authenticated users only.

Below, we explain how to enforce user authentication before allowing a post to be created.


Enforcing Authentication on the Create Post Endpoint

We start by modifying our POST API endpoint for creating posts. In our router file (post.py), we import the necessary modules including the OAuth2 functionality:

from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter
from sqlalchemy.orm import Session
from typing import List
from .. import models, schemas
from ..database import get_db

router = APIRouter(
    prefix="/posts",
    tags=['Posts']
)

@router.get("/", response_model=List[schemas.Post])
def get_posts(db: Session = Depends(get_db)):
    posts = db.query(models.Post).all()
    return posts

When a user attempts to create a post, we add an authentication dependency by extracting the current user using a valid token:

@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.Post)
def create_posts(
    post: schemas.PostCreate,
    db: Session = Depends(get_db),
    user_id: int = Depends(oauth2.get_current_user)
):
    print(user_id)
    new_post = models.Post(**post.dict())
    db.add(new_post)
    db.commit()
    db.refresh(new_post)
    return new_post

The dependency user_id: int = Depends(oauth2.get_current_user) ensures that the endpoint only proceeds if the user is authenticated. If the token is missing or invalid, an error is raised and the post is not created.


Verifying the Token

The function get_current_user extracts the token provided by the user and calls the helper function verify_access_token to decode and validate it. The implementation is as follows:

def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"}
    )
    return verify_access_token(token, credentials_exception)

The verify_access_token function decodes the token using our secret key and algorithm, extracts the user ID from the payload, and validates that the ID is present:

def verify_access_token(token: str, credentials_exception):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        id: str = payload.get("user_id")
        if id is None:
            raise credentials_exception
        token_data = schemas.TokenData(id=id)
    except JWTError:
        raise credentials_exception

    return token_data

Note

If the token does not include a valid user ID or if an error occurs during decoding, the request fails with an HTTP 401 error.


Testing the Protected Endpoint

When you send a POST request to create a post without a valid token, you will receive an error similar to:

{
    "detail": "Not authenticated"
}

To create a post successfully, first obtain a token via the login route then include it in your request header. For example, after obtaining a token from the login endpoint, you might receive a response like:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3ODkwLCJleHBpcmF0aW9uX3RpbWUiOiIxNjA2Mjc3Njg3Iiwicm9sZSI6InVzZXIiLCJ1c2VyX25hbWUiOiJzYW5qZWV2QGdtYWlsLmNvbSIsImZpcnN0X25hbWUiOiJzYW5qZW92In0.F45sS4iRzFo5vdAcrH2lC01S15BE",
    "token_type": "bearer"
}

Include the token in the Authorization header as follows:

Authorization: Bearer <your_token_here>

With the provided token, the API validates the request and permits the creation of the post, returning a response similar to:

{
    "title": "top beaches in florida",
    "content": "something somethings beaches",
    "published": true,
    "id": 34,
    "created_at": "2021-08-21T01:32:22.239498-04:00"
}

In Postman, you can also select the Bearer Token option under the Authorization tab and paste your token. This ensures that the token is correctly sent in the request header.


Protecting Other Routes

You can enforce authentication on other endpoints by adding the current user dependency. Below are examples for fetching an individual post and deleting a post.

Getting an Individual Post

@router.get("/{id}", response_model=schemas.Post)
def get_post(
    id: int,
    db: Session = Depends(get_db),
    user_id: int = Depends(oauth2.get_current_user)
):
    post = db.query(models.Post).filter(models.Post.id == id).first()
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Post with id: {id} does not exist"
        )
    return post

Deleting a Post

@router.delete("/posts/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_post(
    id: int,
    db: Session = Depends(get_db),
    user_id: int = Depends(oauth2.get_current_user)
):
    post_query = db.query(models.Post).filter(models.Post.id == id)
    if post_query.first() is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Post with id: {id} does not exist"
        )
    post_query.delete(synchronize_session=False)
    db.commit()

You can apply similar modifications to update operations to ensure that only authenticated users can modify content.


Setting Up the Login Endpoint

In the authentication router (auth.py), a token schema is used as the response model. This ensures that when a user logs in, the returned response contains exactly the fields defined in our token schema. Make sure the attribute names and capitalization match the schema definitions.

from fastapi import APIRouter, Depends, status, HTTPException, Response
from fastapi.security.oauth2 import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from .. import database, schemas, models, utils, oauth2

router = APIRouter(tags=['Authentication'])

@router.post('/login', response_model=schemas.Token)
def login(
    user_credentials: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(database.get_db)
):
    user = db.query(models.User).filter(
        models.User.email == user_credentials.username
    ).first()

    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Invalid Credentials"
        )
        
    if not utils.verify(user_credentials.password, user.password):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Invalid Credentials"
        )
    
    # (Token generation logic goes here)

When login is successful, you will see a response similar to:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxfQ.ActVWKMnI0OQKurUb-PJkGceJoztIA81Q-xEW",
  "token_type": "bearer"
}

Ensure that your client sends this token with every subsequent request that requires authentication.


Testing All Routes

After securing your endpoints, consider testing these common scenarios:

ScenarioExpected Outcome
GET /postsUnauthorized request returns a 401 error with "Not authenticated".
GET /posts/{id}Requires a valid token to fetch a specific post.
DELETE /posts/{id}Deleting without proper authentication returns a not found or unauthorized error.
PUT /posts/{id}Updating a post without the Bearer token returns "Not authenticated".

Important

Always include a valid token in your requests for protected endpoints to ensure proper access control.


This concludes our guide on protecting routes using authentication in FastAPI. By enforcing these measures, your API will only allow authenticated users to perform sensitive actions such as creating, updating, and deleting posts.

Watch Video

Watch video content

Previous
Verify User Is Logged In