Skip to main content

Welcome to a Grasp lesson

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

Lesson 31

Building a Secure Login with JWTs

Recommended resources

Hello! Welcome back to our course on FastAPI.

In our last lesson, you successfully built a user registration endpoint. You learned how to take user credentials, securely hash the password using bcrypt, and store the user in the database. This was a critical first step in building a secure API.

Today, we'll build the other side of that coin: the login endpoint. This is where a user will exchange their credentials for a JSON Web Token (JWT). This token will then be used to access protected parts of our application.

By the end of this lesson, you will have a fully functional /login (or /token) endpoint that:

  1. Accepts a username and password.
  2. Verifies these credentials against the stored hashed password.
  3. Issues a signed JWT access token upon successful verification.

This brings us one step closer to your goal of building secure, production-ready APIs for your AI models.

Step 1: Installing Dependencies and Initial Configuration

To create and manage JWTs, we need a couple of libraries. We also need a way to configure our security settings, like the secret key that signs the tokens.

First, let's install the required packages. python-jose is a library for handling JWTs, and python-multipart is needed by FastAPI to handle the form data from the login request.

pip install "python-jose[cryptography]" python-multipart

Now, let's create a file to hold our authentication logic and configuration. In the previous lesson, we created auth_utils.py. Let's add our JWT configuration there. For a real application, you would load these from environment variables, but for now, we'll define them as constants.

Open your auth_utils.py and add the following at the top:




# In auth_utils.py
from datetime import datetime, timedelta, timezone
from typing import Optional
from jose import jwt, JWTError
from passlib.context import CryptContext




# Security constants
SECRET_KEY = "a_super_secret_key_that_should_be_in_an_env_file" # IMPORTANT: Change this and keep it secret
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30




# --- Keep your existing pwd_context and hashing functions here ---
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:



    # ... existing function
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:



    # ... existing function
    return pwd_context.verify(plain_password, hashed_password)
  • SECRET_KEY: This is the secret string used to sign our JWTs. Only our server knows this key, which is how we can verify that the tokens we receive haven't been tampered with.
  • ALGORITHM: The specific algorithm used for signing. HS256 is a common and secure choice.
  • ACCESS_TOKEN_EXPIRE_MINUTES: How long a token is valid after it's been issued.

Step 2: Building the Authentication Helper Functions

Just like we created helpers for hashing passwords, we'll create helpers for authenticating a user and creating their token. This keeps our code clean and reusable.

Let's add two new functions to auth_utils.py.

1. authenticate_user

This function will check if a user exists and if the provided password is correct. It will use the verify_password function you created in the last lesson.




# Add this to auth_utils.py, and update your imports in that file
from sqlalchemy.orm import Session
from . import crud # Assuming crud.py is in the same directory




# ... (rest of auth_utils.py)

def authenticate_user(db: Session, username: str, password: str):
    """
    Authenticates a user.

    - Fetches the user by username.
    - Verifies the provided password against the stored hash.
    - Returns the user object on success, otherwise returns False.
    """
    user = crud.get_user_by_username(db, username=username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

This function is the core of our credential validation logic. Notice it returns a generic False whether the user doesn't exist or the password is wrong. This is a security measure to prevent attackers from figuring out which usernames are registered in the system (a "user enumeration" attack).

2. create_access_token

This function will generate the JWT. It takes the data to be encoded (the payload) and an optional expiration time.




# Add this to auth_utils.py

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    """
    Creates a JWT access token.

    - Copies the payload data.
    - Sets an expiration time.
    - Encodes the token using the secret key and algorithm.
    """
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

The payload is a dictionary. The "sub" (subject) claim is a standard way to identify who the token is for—we'll use the username. The "exp" (expiration time) claim is crucial for security, ensuring tokens don't live forever.

For a detailed walkthrough of setting up these utilities, the following video is a great resource.

FastAPI JWT Tutorial | How to add User Authentication

The video "FastAPI JWT Tutorial" by Eric Roby clearly demonstrates how to set up the necessary libraries and create the helper functions for authenticating users and generating tokens.

Watch the following segments: Configuration (2:20 - 5:01): This covers installing dependencies and setting up the secret key, algorithm, and other initial configurations. authenticate_user function (12:00 - 13:20): This part details the logic for verifying user credentials against the database. create_access_token function (14:11 - 15:59): This explains how the JWT is encoded with a payload and expiration time.


Step 3: Defining Schemas and the OAuth2 Scheme

Before building the endpoint, we need two more things:

  1. A Pydantic schema for the token response.
  2. A way to tell FastAPI how to handle the login form and where to find tokens in future requests.

1. Token Schema

In your schemas.py file, add a new model for the token response:




# In schemas.py
from pydantic import BaseModel




# ... (your existing User schemas)

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None

The Token schema defines the shape of the JSON we'll send back on a successful login. We'll use TokenData in the next lesson to represent the data we extract from a token.

2. OAuth2 Password Scheme

FastAPI has built-in support for OAuth2 security schemes. We'll use OAuth2PasswordBearer to signal to FastAPI and the interactive docs that our API uses bearer tokens for authentication.

In your auth_router.py file, add this line near the top:




# In auth_router.py
from fastapi.security import OAuth2PasswordBearer




# ... (other imports)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

The tokenUrl points to the login endpoint we are about to create. This is what enables the "Authorize" button in the Swagger UI.

Swagger UI for Authentication Demo API
This image shows the interactive documentation, highlighting the `/token` endpoint and the padlock icons on protected routes, which appear once you configure an OAuth2 scheme.

Step 4: Implementing the Login Endpoint

Now we can assemble all the pieces into our login endpoint. We will use another FastAPI dependency, OAuth2PasswordRequestForm, which is designed specifically for this purpose. It parses the incoming form data and makes the username and password available.

In your auth_router.py, add the following endpoint:




# In auth_router.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import timedelta




# Import your helper functions and schemas
from . import crud, schemas, auth_utils
from .database import get_db

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




# ... your existing /register endpoint

@router.post("/token", response_model=schemas.Token)
async def login_for_access_token(
    form_data: OAuth2PasswordRequestForm = Depends(), 
    db: Session = Depends(get_db)
):
    user = auth_utils.authenticate_user(
        db, 
        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"},
        )
    
    access_token_expires = timedelta(minutes=auth_utils.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = auth_utils.create_access_token(
        data={"sub": user.username}, 
        expires_delta=access_token_expires
    )
    
    return {"access_token": access_token, "token_type": "bearer"}

Let's break down this endpoint:

  • @router.post("/token", ...): We define a POST endpoint at /auth/token.
  • form_data: OAuth2PasswordRequestForm = Depends(): This is the magic. FastAPI injects a dependency that parses the request body as form data and provides an object with username and password attributes.
  • auth_utils.authenticate_user(...): We call our helper to validate the credentials.
  • raise HTTPException(...): If authentication fails, we return a 401 Unauthorized error.
  • auth_utils.create_access_token(...): If authentication succeeds, we create a token with the user's username as the subject (sub).
  • return {"access_token": ..., "token_type": "bearer"}: We return the token in the format defined by our Token schema.
FastAPI Login with PyJWT Token Authentication Example
This image shows a successful API call where a username and password are exchanged for a JWT access token.

Step 5: Testing Your Login Endpoint

It's time to test our new endpoint. Run your application and go to the interactive docs (/docs).

  1. Locate the POST /auth/token endpoint.
  2. Click "Try it out".
  3. Enter the username and password for the user you created in the previous lesson.
  4. Click "Execute".

If the credentials are correct, you will receive a 200 OK response containing your access_token and token_type. Congratulations, your login system is working!

You can also now click the Authorize button at the top right of the docs. A dialog will pop up where you can paste the entire token (including the word "bearer", or simply the token string depending on the setup) to authenticate your session within the interactive docs. This will be essential for our next lesson.

For another excellent video guide on this process, including testing in the Swagger UI, please see the resource below.

Python FastAPI Tutorial (Part 10): Authentication - Registration and Login with JWT

Corey Schafer's FastAPI tutorial provides a very clear, hands-on implementation of a login endpoint and demonstrates how to test it effectively using the interactive docs.

Watch the following segments: Login Endpoint Implementation (26:37 - 33:05): This part walks through creating the login endpoint within a router, explaining the use of OAuth2PasswordRequestForm and the token generation process. Testing the Login (38:27 - 40:59): This segment shows how to use the interactive docs to log in, receive a token, and use the 'Authorize' button.

For reinforcement, you can also read through a concise text-based implementation.

Login & Registration System with JWT in FastAPI

The GeeksforGeeks article "Login & Registration System with JWT in FastAPI" offers a quick and clear summary of the entire login flow, from helper functions to the final endpoint.

Review 'Step 6: Helper functions (Security)' and the '/login' route within 'Step 8: Routes'. This will provide a solid text-based reference for the code we've written today.


Conclusion

Excellent work! You have now implemented a complete registration and login flow. Users can securely sign up, and they can log in to receive a JWT that proves their identity.

Key Takeaways:

  • OAuth2 Forms: FastAPI's OAuth2PasswordRequestForm simplifies handling standard login requests.
  • JWT Creation: JWTs are created by encoding a payload (containing claims like sub and exp) with a SECRET_KEY and an ALGORITHM.
  • Authentication Flow: The login endpoint orchestrates the process: it uses a helper function to authenticate credentials and another to create a token upon success.
  • Security Best Practices: Returning a generic error on login failure prevents attackers from discovering valid usernames.

You now have the tools to issue tokens. In our next lesson, we'll learn how to consume them. We will build a dependency that can validate an incoming JWT and protect specific endpoints, ensuring only authenticated users can access them.