Back to Blog
fastapi
authentication
jwt
oauth
python
security

How to Implement Authentication in FastAPI: A Complete Developer's Guide

Learn to build secure FastAPI authentication from scratch — user registration, JWT tokens, OAuth, email verification, and password reset with practical code examples

Niklas L.
20 min read

How to Implement Authentication in FastAPI: A Complete Developer's Guide

Building secure authentication in FastAPI doesn't have to be a nightmare. Whether you're creating your first API or you're a seasoned developer looking to implement robust auth, this guide will walk you through everything you need to know about FastAPI authentication.

Authentication is basically the bouncer at your API's door - it checks who's trying to get in and whether they're allowed. In this guide, we'll build a complete authentication system that handles user registration, login, token management, email verification, password resets, and even OAuth with Google.

What is FastAPI Authentication?

FastAPI authentication is the process of verifying user identity and managing access to protected resources in your FastAPI application. Think of it like having a security guard at your building - they check IDs, make sure people are who they say they are, and only let authorized folks into restricted areas.

In practical terms, authentication involves several key pieces: user registration (creating accounts), login (proving identity), token management (maintaining sessions), and session handling to ensure only authorized users can access specific endpoints. It's the foundation that lets you build features like user profiles, protected admin panels, and personalized content.

Why Choose FastAPI for Authentication?

FastAPI makes authentication surprisingly straightforward with its built-in security utilities, automatic OpenAPI documentation, and excellent integration with popular authentication libraries. Plus, it's fast (hence the name) and provides excellent type hints that catch errors before they become problems.

Here's what makes FastAPI particularly great for auth:

Setting Up Your FastAPI Authentication System

Prerequisites

Before we dive in, make sure you have these installed:

pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart sqlalchemy

Project Structure

Here's how I recommend organizing your auth-related files:

app/
├── auth/
│   ├── __init__.py
│   ├── models.py      # User model
│   ├── routes.py      # Auth endpoints
│   ├── services.py    # Auth logic
│   └── validators.py  # Pydantic models
├── config/
│   └── settings.py    # Configuration
└── main.py

Quick Start Option: If you want to skip the manual setup and get a production-ready FastAPI app with authentication, payments, email, AI integration, and more already configured, check out the FastLaunchAPI template. It includes everything we're building in this guide plus a lot more, so you can focus on your business logic instead of boilerplate code.

Core Authentication Components

1. User Model Setup

First, let's create a solid user model that handles the basics. This is where we define what information we store about each user in our database:

from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)
    email = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    is_verified = Column(Boolean, default=False)
    is_admin = Column(Boolean, default=False)
    created_at = Column(DateTime, server_default=func.now())
    reset_token = Column(String, nullable=True)
    google_sub = Column(String, nullable=True)  # For OAuth

Let me break down what each field does:

2. Password Security

Never store plain text passwords. Here's how to handle password hashing properly. Password hashing is like turning your password into a scrambled mess that can't be unscrambled, but we can still check if a password matches:

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

The CryptContext is our password manager. It uses bcrypt, which is a slow hashing algorithm - and that's actually a good thing! It makes it really hard for attackers to crack passwords even if they steal your database.

The hash_password function takes a plain password like "mypassword123" and turns it into something like "$2b$12$xyz...". The verify_password function checks if a plain password matches a hashed one without actually decrypting the hash.

3. JWT Token Management

JSON Web Tokens (JWT) are perfect for stateless authentication. Think of JWTs like digital driver's licenses - they contain information about who you are and when they expire, and they're signed so nobody can fake them:

from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Optional

SECRET_KEY = "your-secret-key-here"  # Use environment variables in production
ALGORITHM = "HS256"

def create_access_token(username: str, user_id: int, expires_delta: timedelta):
    encode = {"sub": username, "id": user_id}
    expires = datetime.utcnow() + expires_delta
    encode.update({"exp": expires})
    return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM)

def decode_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except JWTError:
        return None

def token_expired(token: str) -> bool:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        exp = payload.get("exp")
        return datetime.utcnow() > datetime.fromtimestamp(exp)
    except JWTError:
        return True

Here's what's happening step by step:

  1. create_access_token: This creates a new JWT token. We put the username and user ID inside it, set an expiration time, and sign it with our secret key
  2. decode_token: This reads a JWT token and extracts the information inside. If the token is invalid or tampered with, it returns None
  3. token_expired: This checks if a token has passed its expiration date

The secret key is super important - it's what makes your tokens secure. If someone gets your secret key, they can create fake tokens, so keep it safe!

Essential Authentication Routes

User Registration

Here's a robust user registration endpoint that includes email verification. This is where new users create their accounts:

from fastapi import APIRouter, HTTPException, BackgroundTasks
from sqlalchemy.exc import IntegrityError

@router.post("/create-user", status_code=201)
async def create_user(
    db: db_dependency,
    create_user_request: CreateUserRequest,
    background_tasks: BackgroundTasks
):
    # Check if username exists
    if db.query(User).filter(User.username == create_user_request.username).first():
        raise HTTPException(status_code=400, detail="Username already taken.")

    # Hash the password
    hashed_password = pwd_context.hash(create_user_request.password)

    try:
        user = User(
            username=create_user_request.username,
            email=create_user_request.email,
            hashed_password=hashed_password
        )
        db.add(user)
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=400, detail="Email already taken.")

    # Send verification email
    token = generate_verification_token(create_user_request.username)
    background_tasks.add_task(send_verification_email, create_user_request.email, token)

    return {"message": "User created. Check your email to verify."}

Let's walk through what this registration process does:

  1. Check for duplicates: We first make sure nobody else has that username
  2. Hash the password: We never store the plain password - it gets hashed immediately
  3. Create the user: We make a new User object and save it to the database
  4. Handle errors: If the email is already taken, the database will throw an IntegrityError, which we catch and turn into a user-friendly message
  5. Send verification email: We generate a special token and email it to the user

The BackgroundTasks is really neat - it lets us send the email after we return the response, so the user doesn't have to wait for the email to be sent.

User Login

The login endpoint handles authentication and returns JWT tokens. This is where users prove who they are and get their access credentials:

from fastapi.security import OAuth2PasswordRequestForm

@router.post("/token", response_model=Token)
async def login_for_access_token(
    db: db_dependency,
    form_data: OAuth2PasswordRequestForm = Depends()
):
    user = authenticate_user(form_data.username, form_data.password, db)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid username or password.")

    if not user.is_verified:
        raise HTTPException(status_code=401, detail="Verify your email first.")

    access_token = create_access_token(
        user.username,
        user.id,
        timedelta(days=ACCESS_TOKEN_EXPIRATION_DAYS)
    )
    refresh_token = create_refresh_token(
        user.username,
        user.id,
        timedelta(days=REFRESH_TOKEN_EXPIRATION_DAYS)
    )

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

Here's the login flow broken down:

  1. Get credentials: The OAuth2PasswordRequestForm handles the standard username/password form data
  2. Authenticate: We check if the username exists and the password matches the stored hash
  3. Check verification: We make sure the user has verified their email address
  4. Generate tokens: We create both an access token (for API calls) and a refresh token (for getting new access tokens)
  5. Return tokens: The user gets both tokens back and can start making authenticated requests

The authenticate_user function (which you'd need to implement) would look something like this:

def authenticate_user(username: str, password: str, db):
    user = db.query(User).filter(User.username == username).first()
    if not user or not verify_password(password, user.hashed_password):
        return None
    return user

Token Refresh

Implement token refresh to keep users logged in securely. This is like renewing your driver's license before it expires:

@router.post("/refresh", response_model=Token)
async def refresh_access_token(refresh_token_request: RefreshTokenRequest):
    token = refresh_token_request.refresh_token

    if token_expired(token):
        raise HTTPException(status_code=401, detail="Refresh token expired.")

    user_data = decode_token(token)
    username = user_data.get("sub")
    user_id = user_data.get("id")

    if not username or not user_id:
        raise HTTPException(status_code=401, detail="Invalid token.")

    # Generate new tokens
    access_token = create_access_token(username, user_id, timedelta(days=ACCESS_TOKEN_EXPIRATION_DAYS))
    refresh_token = create_refresh_token(username, user_id, timedelta(days=REFRESH_TOKEN_EXPIRATION_DAYS))

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

The token refresh process works like this:

  1. Check expiration: First, we make sure the refresh token hasn't expired
  2. Extract user info: We decode the refresh token to get the username and user ID
  3. Validate data: We make sure the token contains the information we need
  4. Generate new tokens: We create brand new access and refresh tokens
  5. Return new tokens: The user gets fresh tokens and their session continues

This system lets you keep access tokens short-lived (for security) while still providing a smooth user experience. Users stay logged in as long as they're actively using your app.

Email Verification System

Email verification is crucial for security. It makes sure users actually own the email address they signed up with. Here's how to implement it:

def generate_verification_token(username: str) -> str:
    expire = datetime.utcnow() + timedelta(hours=24)
    to_encode = {"exp": expire, "sub": username}
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@router.get("/verify-email")
async def verify_email(db: db_dependency, token: str):
    try:
        payload = verify_token(token)
        username = payload["sub"]

        user = db.query(User).filter(User.username == username).first()
        if not user:
            raise HTTPException(status_code=404, detail="User not found")

        if user.is_verified:
            return RedirectResponse(f"{FRONTEND_URL}/login?detail=already_verified")

        user.is_verified = True
        db.commit()

        return RedirectResponse(f"{FRONTEND_URL}/login?detail=email_verified")
    except JWTError:
        raise HTTPException(status_code=400, detail="Invalid verification token")

The email verification process works in two parts:

Part 1 - Generating the token: When a user registers, we create a special verification token that expires in 24 hours. This token contains the username and is signed with our secret key.

Part 2 - Verifying the email: When the user clicks the link in their email, they hit this endpoint. We:

  1. Decode the token: Extract the username from the verification token
  2. Find the user: Look up the user in our database
  3. Check status: If they're already verified, we let them know
  4. Mark as verified: Set is_verified to True and save to database
  5. Redirect: Send them back to your frontend with a success message

The redirect URLs include query parameters that your frontend can use to show appropriate messages to the user.

Password Reset Functionality

Users forget passwords all the time. Here's a secure reset system that handles this gracefully:

@router.post("/request-password-reset")
async def request_password_reset(
    reset_request: PasswordResetRequest,
    db: db_dependency,
    background_tasks: BackgroundTasks
):
    user = db.query(User).filter(User.email == reset_request.email).first()

    # Always return the same message for security
    if not user:
        return {"detail": "If the email exists, a reset link has been sent."}

    token = create_reset_token(user.email, timedelta(hours=1))
    user.reset_token = token
    db.commit()

    background_tasks.add_task(send_password_reset_email, user.email, token)
    return {"detail": "If the email exists, a reset link has been sent."}

@router.post("/reset-password")
async def reset_password(reset_data: PasswordReset, db: db_dependency):
    try:
        payload = verify_token(reset_data.token)
        email = payload.get("email")

        if not email:
            raise HTTPException(status_code=400, detail="Invalid token")

        user = db.query(User).filter(User.email == email).first()
        if not user or user.reset_token != reset_data.token:
            raise HTTPException(status_code=400, detail="Invalid reset token")

        user.hashed_password = pwd_context.hash(reset_data.new_password)
        user.reset_token = None
        db.commit()

        return {"detail": "Password reset successful"}
    except JWTError:
        raise HTTPException(status_code=400, detail="Invalid reset token")

The password reset flow has two main parts:

Step 1 - Request reset: When someone forgets their password:

  1. They provide their email address
  2. We look up if that email exists in our database
  3. If it exists, we generate a reset token (expires in 1 hour) and save it to the user record
  4. We send an email with a reset link containing the token
  5. Important: We always return the same message, whether the email exists or not. This prevents attackers from figuring out which emails are registered

Step 2 - Actually reset: When they click the link in their email:

  1. We decode the reset token to get the email address
  2. We find the user and check that the token matches what we stored in the database
  3. We hash the new password and save it
  4. We clear the reset token so it can't be used again

The double-check (token in database matches token in request) prevents someone from reusing old reset tokens or using tokens after they've been used.

OAuth Integration

OAuth makes user registration frictionless. Here's how to add Google OAuth:

from authlib.integrations.starlette_client import OAuth

oauth = OAuth()
oauth.register(
    name='google',
    client_id='your-google-client-id',
    client_secret='your-google-client-secret',
    server_metadata_url='https://accounts.google.com/.well-known/openid_configuration',
    client_kwargs={
        'scope': 'openid email profile'
    }
)

@router.get("/oauth/google")
async def login_oauth_google(request: Request):
    redirect_uri = "http://localhost:8000/auth/oauth/callback/google"
    return await oauth.google.authorize_redirect(request, redirect_uri)

@router.get("/oauth/callback/google")
async def auth_oauth_callback_google(request: Request, db: db_dependency):
    try:
        token = await oauth.google.authorize_access_token(request)
        user_info = token.get('userinfo')

        # Create or get existing user
        user = db.query(User).filter(User.email == user_info['email']).first()
        if not user:
            user = User(
                username=user_info['email'],
                email=user_info['email'],
                hashed_password="",  # OAuth users don't need passwords
                is_verified=True,
                google_sub=user_info['sub']
            )
            db.add(user)
            db.commit()

        # Generate tokens
        access_token = create_access_token(user.username, user.id, timedelta(days=7))
        refresh_token = create_refresh_token(user.username, user.id, timedelta(days=30))

        return RedirectResponse(f"{FRONTEND_URL}/auth?access_token={access_token}&refresh_token={refresh_token}")

    except Exception as e:
        raise HTTPException(status_code=401, detail="OAuth authentication failed")

Protecting Routes with Dependencies

Create reusable dependencies to protect your endpoints. This is where FastAPI really shines - you can protect routes with just a simple dependency injection:

from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer

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

async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    try:
        payload = decode_token(token)
        username: str = payload.get("sub")
        user_id: int = payload.get("id")

        if username is None or user_id is None:
            raise HTTPException(status_code=401, detail="Could not validate credentials")

    except JWTError:
        raise HTTPException(status_code=401, detail="Could not validate credentials")

    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=401, detail="User not found")

    return user

# Usage in protected routes
@app.get("/protected")
async def protected_route(current_user: User = Depends(get_current_user)):
    return {"message": f"Hello {current_user.username}!"}

Here's how route protection works:

The OAuth2PasswordBearer: This tells FastAPI to look for a bearer token in the Authorization header (like "Bearer your-jwt-token-here"). The tokenUrl parameter tells FastAPI where users can get tokens.

The get_current_user dependency: This function runs before your protected route and:

  1. Extracts the token: Gets the JWT token from the Authorization header
  2. Decodes it: Extracts the username and user ID from the token
  3. Validates the data: Makes sure the token contains the required information
  4. Looks up the user: Finds the actual user record in the database
  5. Returns the user: Makes the User object available to your route function

Using the dependency: Any route that includes current_user: User = Depends(get_current_user) will automatically be protected. If someone tries to access it without a valid token, they'll get a 401 error.

The beautiful thing about FastAPI dependencies is that they're composable. You can create additional dependencies for admin-only routes, role-based access, or any other authorization logic you need.

Security Best Practices

1. Environment Variables

Never hardcode secrets in your code. Use environment variables to keep sensitive information secure:

import os
from pydantic import BaseSettings

class Settings(BaseSettings):
    SECRET_KEY: str = os.getenv("SECRET_KEY", "fallback-key-for-development")
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7

    class Config:
        env_file = ".env"

settings = Settings()

This approach keeps your secrets safe by:

Create a .env file in your project root like this:

SECRET_KEY=your-super-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/dbname
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

2. Rate Limiting

Prevent brute force attacks by limiting how many requests users can make:

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@router.post("/token")
@limiter.limit("5/minute")
async def login_for_access_token(request: Request, ...):
    # Login logic here

Rate limiting is crucial for security because it:

The @limiter.limit("5/minute") decorator allows only 5 login attempts per minute per IP address. You can adjust this based on your needs - maybe allow 10 attempts for login but only 3 for password reset requests.

3. HTTPS Only

Always use HTTPS in production and set secure cookie flags. HTTP sends everything in plain text, including passwords and tokens. HTTPS encrypts all communication between your users and your server.

4. Token Expiration

Keep access tokens short-lived (15-30 minutes) and refresh tokens longer (7-30 days). This limits the damage if a token gets stolen:

Testing Your Authentication

Here's a simple test to verify your auth works. Testing authentication is crucial because security bugs can be catastrophic:

import pytest
from fastapi.testclient import TestClient

def test_user_registration(client: TestClient):
    response = client.post("/auth/create-user", json={
        "username": "testuser",
        "email": "[email protected]",
        "password": "secretpassword"
    })
    assert response.status_code == 201
    assert "User created" in response.json()["message"]

def test_user_login(client: TestClient):
    # First create a user
    client.post("/auth/create-user", json={
        "username": "testuser",
        "email": "[email protected]",
        "password": "secretpassword"
    })

    # Then try to login
    response = client.post("/auth/token", data={
        "username": "testuser",
        "password": "secretpassword"
    })
    assert response.status_code == 200
    assert "access_token" in response.json()

These tests cover the basic authentication flow:

Registration test: Creates a new user and verifies:

Login test: Tests the full login flow by:

You should also test edge cases like:

Testing gives you confidence that your authentication system works correctly and helps catch regressions when you make changes.

Common Pitfalls to Avoid

1. Storing Passwords in Plain Text

Always hash passwords. No exceptions. Even in development, even for test accounts. Get into the habit of never storing plain text passwords anywhere.

2. Using Weak Secret Keys

Generate strong, random secret keys and rotate them regularly. Your JWT secret key should be at least 32 characters long and contain random letters, numbers, and symbols. Never use something like "mysecretkey" or "password123".

3. Not Validating Input

Always validate and sanitize user input to prevent injection attacks. Use Pydantic models to validate request data, and never trust user input directly.

4. Ignoring Token Expiration

Implement proper token expiration and refresh mechanisms. Tokens that never expire are a huge security risk. If someone steals a token, you want it to become useless eventually.

5. Not Using HTTPS

Authentication without HTTPS is like leaving your front door wide open. All the password hashing and JWT signing in the world won't help if an attacker can just intercept the password in transit.

6. Returning Too Much Information in Error Messages

Don't tell attackers whether a username exists or not. Instead of "Username not found" vs "Invalid password", always return "Invalid username or password".

7. Not Implementing Rate Limiting

Without rate limiting, attackers can try millions of password combinations. Always limit login attempts, registration attempts, and password reset requests.

FAQs about FastAPI Authentication

Q: What's the difference between authentication and authorization?

A: Authentication verifies who you are (login), while authorization determines what you can do (permissions). Authentication comes first, then authorization.

Q: Should I use JWT tokens or sessions for FastAPI authentication?

A: JWT tokens are generally better for APIs because they're stateless and work well in distributed systems. Sessions are good for traditional web apps but require server-side storage.

Q: How long should access tokens last?

A: Keep access tokens short-lived (15-30 minutes) for security. Use refresh tokens for longer sessions (7-30 days).

Q: Is it safe to store JWT tokens in localStorage?

A: It's safer to store them in httpOnly cookies to prevent XSS attacks. If you must use localStorage, ensure your app is protected against XSS.

Q: How do I handle password complexity requirements?

A: Use Pydantic validators to enforce password rules:

from pydantic import validator
import re

class CreateUserRequest(BaseModel):
    password: str

    @validator('password')
    def validate_password(cls, v):
        if len(v) < 8:
            raise ValueError('Password must be at least 8 characters')
        if not re.search(r'[A-Z]', v):
            raise ValueError('Password must contain uppercase letter')
        if not re.search(r'[a-z]', v):
            raise ValueError('Password must contain lowercase letter')
        if not re.search(r'\d', v):
            raise ValueError('Password must contain a number')
        return v

Q: How do I implement role-based access control?

A: Add a role field to your User model and create role-checking dependencies:

from enum import Enum

class UserRole(str, Enum):
    USER = "user"
    ADMIN = "admin"
    MODERATOR = "moderator"

def require_role(required_role: UserRole):
    def role_checker(current_user: User = Depends(get_current_user)):
        if current_user.role != required_role:
            raise HTTPException(status_code=403, detail="Insufficient permissions")
        return current_user
    return role_checker

# Usage
@app.get("/admin")
async def admin_only(user: User = Depends(require_role(UserRole.ADMIN))):
    return {"message": "Admin access granted"}

Q: What's the best way to handle logout with JWT tokens?

A: Since JWT tokens are stateless, you can't really "logout" server-side. Instead, implement token blacklisting or use short-lived tokens with automatic refresh.

Q: How do I implement "Remember Me" functionality?

A: Issue longer-lived refresh tokens when the user checks "Remember Me", otherwise use shorter expiration times.

Q: Should I hash refresh tokens?

A: Yes, store refresh tokens hashed in your database for additional security:

import hashlib

def hash_token(token: str) -> str:
    return hashlib.sha256(token.encode()).hexdigest()

Conclusion

Implementing authentication in FastAPI doesn't have to be overwhelming. Start with the basics - user registration, login, and token management - then gradually add features like email verification, password reset, and OAuth.

Remember that security is not a one-time setup but an ongoing process. Keep your dependencies updated, monitor for vulnerabilities, and always follow security best practices.

The code examples in this guide give you a solid foundation, but every application has unique requirements. Adapt these patterns to fit your specific needs, and don't hesitate to dive deeper into FastAPI's excellent documentation for more advanced features.

Want to Skip the Setup? If you're looking to get started quickly with a production-ready FastAPI application, consider using the FastLaunchAPI template. It includes all the authentication features we've covered in this guide, plus integrated payments (Stripe), email systems, AI capabilities, database setup, deployment configurations, and more. Sometimes it's worth investing in a solid foundation so you can focus on building your unique features instead of recreating the same boilerplate code everyone needs.

Whether you build from scratch using this guide or start with a template, the important thing is understanding how authentication works under the hood. That knowledge will serve you well as you build and scale your applications.

Happy coding, and stay secure!

Related Articles