Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Secure Login with JWT and OAuth2

Recommended resources

Hello! Welcome back to our module on Authentication and Authorization in FastAPI.

In the last lesson, we built the essential utility functions for security: one for hashing and verifying passwords (pwdlib) and others for creating and decoding JWTs (PyJWT). We now have the building blocks, but they aren't connected to our API. A user has no way to actually request a token.

Today, we'll bridge that gap. Our goal is to build a login endpoint that returns a JWT token upon valid credentials using an OAuth2PasswordRequestForm dependency. This endpoint will be the public gateway for users to authenticate themselves.

1. The "Password Flow" and FastAPI's Helper

In your work with Django REST Framework, you've likely used an endpoint from a library like django-rest-framework-simplejwt (e.g., TokenObtainPairView). You send a username and password, and you get back an access and refresh token. This standardized process is part of the OAuth2 specification and is known as the "Resource Owner Password Credentials Grant" flow, or more simply, the "password flow".

The key detail is that this flow traditionally expects the credentials to be sent not as JSON, but as form data (application/x-www-form-urlencoded).

FastAPI provides a dependency to handle this specific format for you: OAuth2PasswordRequestForm. When you use it in your path operation, FastAPI knows to:

  1. Expect form data with username and password fields.
  2. Parse this data and make it available to your function.
  3. Automatically update your OpenAPI documentation to show the correct input fields.

This is what powers the "Authorize" button you often see in Swagger UI. By creating an endpoint that provides a token, you allow yourself (and other developers) to authenticate directly within the interactive docs to test protected endpoints.

FastAPI Swagger UI with Authentication Endpoints
This is an example of a FastAPI application's Swagger UI. The `/users/login` endpoint we are about to build enables the "Authorize" button, allowing you to paste a token and access protected endpoints (like the ones with the padlock icon) directly from the documentation.

2. Building the Login Endpoint

Let's construct the endpoint. The process involves three main steps:

  1. Accept the user's credentials.
  2. Authenticate them against our database.
  3. If successful, generate and return a JWT.

The official FastAPI documentation provides a superb, complete example of this. I encourage you to read through it as our primary reference for this section.

OAuth2 with Password (and hashing), Bearer with JWT tokens

This is the official FastAPI documentation on OAuth2 with JWT. It covers the entire flow, from helper functions to the final endpoint.

Please read the section titled 'Update the /token path operation'. It contains the complete code for the login endpoint. Pay close attention to how login_for_access_token uses OAuth2PasswordRequestForm = Depends() (or the Annotated version) to get the form data.

Now, let's assemble the code for our endpoint, which we can place in an auth router file. This code assumes you have the authenticate_user and create_access_token functions from our previous lessons.

from datetime import timedelta
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm




# Assuming these are in your project structure
from . import utils  # Where create_access_token lives
from . import schemas # Where your Pydantic models live
from . import crud    # Where authenticate_user would be if using a DB

router = APIRouter(
    prefix="/auth",
    tags=["Authentication"]
)

@router.post("/token", response_model=schemas.Token)
async def login_for_access_token(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
):



    # 1. Authenticate the user
    #    This function should find the user and verify the password.
    #    It returns the user object on success, or False on failure.
    user = crud.authenticate_user(
        username=form_data.username, 
        password=form_data.password
    )
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
        



    # 2. Create the token
    access_token_expires = timedelta(minutes=utils.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = utils.create_access_token(
        data={"sub": user.username},  # 'sub' is the standard claim for subject (user identifier)
        expires_delta=access_token_expires
    )
    



    # 3. Return the token
    return {"access_token": access_token, "token_type": "bearer"}

Let's break down this function:

  • @router.post("/token", response_model=schemas.Token): We define a POST endpoint at /token. The response_model ensures our output matches the Token Pydantic schema (which should contain access_token and token_type fields).
  • form_data: Annotated[OAuth2PasswordRequestForm, Depends()]: This is the core of the magic. We use dependency injection to get an instance of OAuth2PasswordRequestForm. The form_data object will have .username and .password attributes containing the user's input.
  • user = crud.authenticate_user(...): We call our authentication logic. This function is responsible for finding the user and checking if the provided password hash matches the one in the database.
  • if not user: raise HTTPException(...): If authentication fails, we immediately stop and return a 401 Unauthorized error. It's crucial to provide a generic error message to prevent attackers from discovering valid usernames (a "user enumeration" attack). The WWW-Authenticate: Bearer header is part of the OAuth2 standard.
  • access_token = utils.create_access_token(...): If authentication succeeds, we call the token creation utility from our last lesson. We pass the user's unique identifier (here, the username) as the sub (subject) claim.
  • return {"access_token": access_token, "token_type": "bearer"}: Finally, we return the newly created token. The token_type "bearer" tells the client how to use it (i.e., in an Authorization: Bearer <token> header).

3. A Visual Walkthrough

Reading code is one thing; seeing it implemented and run is another. The following video provides a clear, step-by-step demonstration of building this exact endpoint.

Fast API Tutorial, Part 27: Security with JWT

This video from JVP Design walks through creating the /token route. Watching someone else code it can help solidify the concepts and flow.

Watch the segment from 00:12:27 to 00:16:24. The presenter implements the login_for_access_token function, explaining each part as they go, from injecting the OAuth2PasswordRequestForm to raising the HTTPException on failure.

This video nicely ties together the authentication helpers (like authenticate_user) with the endpoint logic, showing how they cooperate to secure your application.


4. Integrating into a Larger Project

As you are interested in modern project structures, it's worth seeing how this login route fits into a well-organized application. The code snippet I provided earlier used an APIRouter. This is the standard FastAPI way to organize your endpoints, analogous to including app-specific urls.py files in Django.

The next video shows this login endpoint being created within a dedicated auth router as part of a larger, structured project.

FastAPI JWT Authentication 2025 (JSON Web Tokens)

This video from Tenacity demonstrates building the login endpoint within a more structured project, using an APIRouter to organize the authentication-related routes.

Watch from 00:13:03 to 00:15:12. Focus on how the @router.post('/token') is defined within a dedicated authentication router file. This aligns with your goal of learning professional code structuring.


Conclusion

Excellent work! You have now built the front door to your API's authenticated features. By creating a login endpoint, you've connected your user data, password verification logic, and JWT generation into a cohesive authentication flow.

Key Takeaways:

  • The OAuth2 "password flow" is a standard for exchanging a username and password for an access token.
  • FastAPI's OAuth2PasswordRequestForm is a dependency that handles parsing x-www-form-urlencoded credentials.
  • The login endpoint acts as an orchestrator: it uses the authenticate_user helper to verify credentials and the create_access_token helper to generate a JWT.
  • Failed login attempts should raise a 401 Unauthorized HTTPException with a generic error message to enhance security.
  • The generated JWT is returned to the client, typically with a token_type of "bearer".

Preview of the Next Lesson:

A user can now log in and receive a token, but this token doesn't do anything yet. Our other endpoints are still wide open. In the next lesson, we will solve this by implementing a dependency that extracts and validates a JWT from request headers using OAuth2PasswordBearer. This will allow us to protect routes and identify the currently authenticated user for any given request.