Implementing JWT Authentication in FastAPI

Learn how to secure your FastAPI endpoints by implementing JWT-based authentication with user login and token validation.

Jun 20, 2025 10 min read

Photo by Jarrod Erbe on Unsplash

When building modern web applications, securing your API endpoints is a must. One popular and efficient way to handle authentication is by using JSON Web Token (JWT). It allows your backend to verify user identity without maintaining session state on the server.

In this post, I’ll show you how to implement JWT authentication in a FastAPI app. You’ll learn how to generate tokens, authenticate users, and protect your API routes.

What is JWT?

JSON Web Token (JWT) in an open standard that defines a compact, URL-safe way to securely transmit information between parties. They are commonly used for authentication and information exchange in web applications.

A JWT is basically a string made up of three parts separated by dots:

  header.payload.signature

Header - contains the type of token (JWT) and the signing algorithm.
Payload - contains the actual data (like user ID, roles, etc).
Signature - used to verify that the sender is who they say they are and to ensure the token hasn’t been tampered with.

Here is a sample JWT:

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTc1NTEzOTAyMiwiZXhwIjoxNzU2MjU5MDIyfQ.SW7hgzi-Ho0tpSg9RyUHnUzEZoxuILlPr4M5PzPfSXY

Try to copy the token above and paste it to jwt.io to see the decoded header and payload. You will see the following header:

JSON
{ 
  "alg": "HS256", 
  "typ": "JWT" 
}

Which means that the token is signed with HMAC using the SHA-256 algorithm, and the type of the token is JWT.

You will also see the following payload:

JSON
{ 
  "sub": "12345", 
  "name": "John Doe", 
  "iat": 1755139022, 
  "exp": 1756259022 
}

This will be the JSON that contains the information about your user. sub (Subject) is the unique identifier for the user. iat (Issued At) and exp (Expiration Time) are both Unix timestamp in seconds.

Note that you can also add your custom claims/attributes to the payload, such as name in the example above.

How do I generate JWT for my user?

In your /login endpoint, accept username and password fields submitted by your user. Fetch the user from your database that matches with the given username and password. Using that data, construct the payload for JWT with the attributes you need, for example.:

Python
{ 
  "sub": "c441200b-4e37-42d5-86e8-1c6ab43328ca", 
  "name": "Jane Doe", 
  "email": "jane.doe@example.com", 
  "exp": 1857257031, 
}

Generate the JWT using a Python library like python-jose, signing it with your secret key. This will be the access token, and it should be short-lived for security purpose.

In addition to access token, you also need to generate another JWT using the same payload but with longer expiration time. This will be the refresh token, and it will be used to request the new access token instead of re-login using username and password again.

Send both the access token and refresh token back to the client. They should include the access token in the Authorization header as a Bearer token in subsequent API calls.

How do the client or frontend send the JWT for authentication?

After the client receives the JWT, they need to include it in the Authorization header when making requests to protected endpoints. The token is sent as a Bearer token.

For example, if using Curl:

Shell
curl -X GET https://api.yoursite.com/protected \ 
     -H "Authorization: Bearer <Your JWT Token>"

Or, if the client is a frontend application, they can make the requests using fetch like the following:

JavaScript
fetch("https://api.yoursite.com/protected", { 
  method: "GET",  
  headers: { 
    "Authorization": "Bearer <Your JWT Token>" 
  }  
})  
.then(response => response.json())  
.then(data => console.log(data))  
.catch(err => console.error(err));

Always store tokens securely on the client side like in localStorage or sessionStorage for web applications.

How do I verify the JWT on the backend?

When your backend receives a request with JWT, grab the token from the Authorization header, decode and verify it using your secret key. Make sure that the token is valid and hasn’t expired.

If the token is valid, use the payload in the JWT to fetch the corresponding user from your database. This user will be treated as the currently logged-in user.

If the token has expired, return the 401 (Unauthorized) status code to the user. Upon receiving this status code, the client/frontend should be aware that their token is no longer valid, and they need to obtain the new access token.

Let’s get started!

Let’s create a FastAPI application for the JWT authentication. We will make things very simple; we will write all the code in a single main.py file. We’ll also store the sample users in a Python list instead of using a real database.

We will be using uv for our project manager.

First, create new project:

Shell
uv init fastapi-app
cd fastapi-app

Install the required packages for our project:

Shell
uv add fastapi[standard] bcrypt python-jose

We are installing fastapi[standard], which will install the FastAPI framework plus some extras like uvicorn, pydantic, httpx, and some other packages.

The bcrypt package will be used for hashing the passwords.

The python-jose package will be used for signing and verifying the JWT.

Now your directory should look similar like this:

  
fastapi-app/
  .git/
  .venv/
  .gitignore
  main.py 
  pyproject.toml
  README.md
  uv.lock

Open the main.py file, and write basic FastAPI app code:

Python
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def home():
    return {"message": "Hello, World!"}

Run the dev server:

Shell
uv run fastapi dev

Open http://localhost:8000 using your browser and it should display a JSON with the “Hello, World!” message.

Setup the users database and the helper functions

Like I mentioned earlier, we will store the users in a Python list instead of using a real database to make things simple.

Take a look at this code:

Python
import bcrypt
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    username: str
    hashed_password: bytes

def get_password_hash(plain_password: str) -> bytes:
    """
    Return hashed password from plain password.
    """
    bytes = plain_password.encode("utf-8")
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(bytes, salt)

users_db = [
    {
        "id": 1,
        "name": "John Doe",
        "username": "johndoe",
        "hashed_password": get_password_hash("password123"),
    },
    {
        "id": 2,
        "name": "Jane Doe",
        "username": "janedoe",
        "hashed_password": get_password_hash("password456"),
    }
]

We created a Pydantic schema User to represent a user and stored the sample users in the users_db list. Note that we don’t store the passwords as plain text, we are storing the hashed passwords instead using the get_password_hash function.

While we are at it, let’s write another helper functions:

Python
def verify_password(plain_password: str, hashed_password: bytes) -> bool:
    """
    Verify if a plain password matches a hashed password.
    """
    bytes = plain_password.encode('utf-8')
    return bcrypt.checkpw(bytes, hashed_password)

def get_user(db, username: str) -> User | None:
    """
    Retrieve a user from the database by their username.
    """
    for user_dict in db:
        if user_dict["username"] == username:
            return User(**user_dict)

def authenticate_user(db, username: str, password: str) -> User | bool:
    """
    Authenticate a user by their username and password.
    """
    user = get_user(db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

The functions above are self-explanatory.

Write the login endpoint

In the /login endpoint, we accept username and password fields submitted by the user. Then we fetch the user from the database and return the generated JWT.

Write a new helper to create the access token:

Python
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError

...

SECRET_KEY = "my-secret-key"  # In production, use a proper secret key
ALGORITHM = "HS256"

def create_access_token(data: dict, expires_in: int = None):
    to_encode = data.copy()
    if 'sub' in to_encode:
        to_encode['sub'] = str(to_encode['sub']) # sub needs to be a string

    if expires_in:
        expire = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
    else:
        expire = datetime.now(timezone.utc) + timedelta(seconds=300)

    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

    return encoded_jwt

def create_refresh_token(data: dict, expires_in: int = 86400):
    return create_access_token(data, expires_in)

We will use the create_access_token function above to create the access token for the given payload. By default, the access token will be valid for 5 minutes. We also define the create_refresh_token function to create the refresh token that will be valid for 24 hours.

Now we have all of the helper functions needed to create the /login endpoint.

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

...

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

@app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()) -> Token:
    user = authenticate_user(users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    data = {"sub": user.id, "username": user.username}
    access_token = create_access_token(data)
    refresh_token = create_refresh_token(data)

    return Token(
        access_token=access_token,
        refresh_token=refresh_token,
        token_type="bearer"
    )

Since we are just wiring up the helper functions in the /login endpoint above, the code is self-explanatory.

One thing to note is we’re using OAuth2PasswordRequestForm, a FastAPI dependency class that parses application/x-www-form-urlencoded form data (like a classic HTML form submission) and pulls the data from the form fields into its username and password attributes. This is necessary because OAuth2 login flows typically send credentials as form data instead of JSON.

Write the protected endpoint

Now let’s create a protected endpoint that can be accessed by authorized users only. We’ll start by writing a dependency that verifies the JWT and retrieve the current user.

Python
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

async 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"},
    )

    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("username")
        user = get_user(users_db, username)
        if user is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    return user

The get_current_user function takes the token from the request header, decodes it, and check if it is valid. If the token is valid, it will return the authenticated user object. Otherwise it raises the 401 Unauthorized exception.

We’ll use this dependency in our protected endpoint:

Python
@app.get("/protected")
async def protected(current_user: User = Depends(get_current_user)):
    return {"message": "You are accessing a protected route."}

We are declaring our route to depends on the get_current_user function. When a request hits this endpoint, FastAPI will run the get_current_user function first, and store the return value in the current_user variable.

Now, if you try to hit /protected without a valid token, you’ll get a 401 Unauthorized error. If you provide a valid token, you’ll see the message.

Write the refresh token endpoint

When the access token expires, the client needs to request a new one by sending their refresh token to our endpoint. This allows users to stay logged in without having to enter their credentials again.

Let’s create the /token/refresh endpoint:

Python
class RefreshTokenRequest(BaseModel):
    refresh_token: str

@app.post("/token/refresh")
async def get_new_access_token(data: RefreshTokenRequest):
    user = await get_current_user(data.refresh_token)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Could not validate refresh token",
            headers={"WWW-Authenticate": "Bearer"},
        )

    payload = {"sub": user.id, "username": user.username}
    access_token = create_access_token(payload)

    return Token(
        access_token=access_token,
        refresh_token=data.refresh_token,
        token_type="bearer"
    )

The endpoint expects the client to send the refresh token in the request body as JSON, like this:

  {"refresh_token": "string"}

It then validates the refresh token and retrieves the user. Since the refresh token is also a JWT (just like the access token), we can reuse our get_current_user function to handle the validation. If everything is okay, the endpoint returns a new access token for the user.

Summary

We have written a sample FastAPI application with JWT authentication. We created the /login endpoint to login using username and password, protect the /protected route for authenticated users only, and created the /token/refresh to get new access token.

To get the access token, submit the username and password to the /login endpoint:

Shell
curl -X POST http://localhost:8000/login \
     -d "username=johndoe" \
     -d "password=password123"

It will return the access token and the refresh token:

  
{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer"
}

Access the protected route by specifying the access token in the Authorization header:

Shell
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" http://localhost:8000/protected

When the token has expired, request for new access token by specifying the refresh token in the request body:

Shell
curl -X POST http://localhost:8000/token/refresh \
     -H "Content-Type: application/json" \
     -d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'

The full source code is available in my FastAPI examples repository.