Skip to main content
Cookie-based authentication stores JWT tokens in HTTP-only cookies, providing better security for web applications by preventing JavaScript access to tokens. This protects against XSS attacks while maintaining a seamless user experience.

Why use cookies

Cookies offer several security advantages over storing tokens in JavaScript:

XSS protection

HTTP-only cookies cannot be accessed by JavaScript, protecting tokens from XSS attacks

Automatic sending

Browsers automatically include cookies in requests, no manual header management needed

Secure flag

Cookies can be marked as secure to only send over HTTPS

SameSite protection

SameSite attribute protects against CSRF attacks
CSRF protection required: When using cookies, you must enable CSRF protection to prevent cross-site request forgery attacks.

Configuration

Configure AuthX to use cookies:
from datetime import timedelta
from authx import AuthX, AuthXConfig

auth_config = AuthXConfig(
    JWT_ALGORITHM="HS256",
    JWT_SECRET_KEY="your-secret-key",
    # Enable cookie-based authentication
    JWT_TOKEN_LOCATION=["cookies"],
    
    # Cookie settings
    JWT_COOKIE_SECURE=True,          # Only send over HTTPS (disable for local dev)
    JWT_COOKIE_HTTP_ONLY=True,       # Prevent JavaScript access
    JWT_COOKIE_SAMESITE="lax",       # CSRF protection ("strict", "lax", or "none")
    JWT_COOKIE_DOMAIN=None,          # Set to your domain in production
    JWT_COOKIE_MAX_AGE=None,         # Use session cookies (None) or set max age
    
    # Cookie names
    JWT_ACCESS_COOKIE_NAME="access_token_cookie",
    JWT_REFRESH_COOKIE_NAME="refresh_token_cookie",
    
    # Cookie paths
    JWT_ACCESS_COOKIE_PATH="/",
    JWT_REFRESH_COOKIE_PATH="/",
    
    # CSRF protection (required for cookies)
    JWT_COOKIE_CSRF_PROTECT=True,
    JWT_CSRF_IN_COOKIES=True,
    JWT_CSRF_METHODS=["POST", "PUT", "PATCH", "DELETE"],
    
    # CSRF cookie names and headers
    JWT_ACCESS_CSRF_COOKIE_NAME="csrf_access_token",
    JWT_ACCESS_CSRF_HEADER_NAME="X-CSRF-TOKEN",
    JWT_REFRESH_CSRF_COOKIE_NAME="csrf_refresh_token",
    JWT_REFRESH_CSRF_HEADER_NAME="X-CSRF-TOKEN",
)

auth = AuthX(config=auth_config)
Set JWT_COOKIE_SECURE=False for local development (HTTP). Always use True in production with HTTPS.

Setting cookies on login

Set tokens in cookies using the Response object:
from fastapi import FastAPI, HTTPException, Response
from pydantic import BaseModel

app = FastAPI()
auth.handle_errors(app)

class LoginRequest(BaseModel):
    username: str
    password: str

@app.post("/login")
def login(data: LoginRequest, response: Response):
    """Login and set tokens in HTTP-only cookies."""
    # Validate credentials
    if data.username in USERS and USERS[data.username]["password"] == data.password:
        # Create tokens
        access_token = auth.create_access_token(uid=data.username)
        refresh_token = auth.create_refresh_token(uid=data.username)
        
        # Set tokens in HTTP-only cookies
        auth.set_access_cookies(access_token, response)
        auth.set_refresh_cookies(refresh_token, response)
        
        return {
            "message": "Login successful",
            "username": data.username
        }
    
    raise HTTPException(status_code=401, detail="Invalid credentials")
Tokens are automatically included in subsequent requests by the browser. No need to manually pass them in headers.

Accessing protected routes

AuthX automatically reads tokens from cookies:
from authx.schema import TokenPayload

@app.get("/protected")
async def protected_route(payload: TokenPayload = auth.ACCESS_REQUIRED):
    """Protected route - token is read from cookie automatically."""
    return {
        "message": "Access granted",
        "username": payload.sub,
    }
No Authorization header needed:
# Cookie is sent automatically by the browser
curl -X GET http://localhost:8000/protected \
  --cookie "access_token_cookie=eyJhbGci..."

CSRF protection

When using cookies, CSRF protection is essential:

How CSRF protection works

1

Login sets CSRF token

Server sets both the JWT cookie (HTTP-only) and a CSRF token cookie (readable by JavaScript).
2

Client reads CSRF token

JavaScript reads the CSRF token from the non-HTTP-only cookie.
3

Include in request

Client includes CSRF token in request header for state-changing operations (POST, PUT, DELETE).
4

Server validates

Server verifies that the CSRF token in the header matches the one in the JWT.

Making CSRF-protected requests

@app.post("/posts")
async def create_post(payload: TokenPayload = auth.ACCESS_REQUIRED):
    """Create post - CSRF protection is automatic for POST requests."""
    return {"message": "Post created", "user": payload.sub}
Client must include CSRF token:
// JavaScript client
const csrfToken = getCookie('csrf_access_token');

fetch('http://localhost:8000/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': csrfToken  // Required for cookie-based auth
    },
    credentials: 'include',  // Include cookies
    body: JSON.stringify({title: 'New Post'})
});

function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

Disabling CSRF for specific routes

@app.post("/webhook")
async def webhook(request: Request):
    """Webhook endpoint - disable CSRF verification."""
    token = await auth.get_access_token_from_request(request)
    payload = auth.verify_token(token, verify_csrf=False)
    
    return {"message": "Webhook processed"}

Logout and clearing cookies

Clear cookies to log out:
@app.post("/logout")
def logout(response: Response):
    """Logout by clearing token cookies."""
    # Clear both access and refresh cookies
    auth.unset_cookies(response)
    
    return {"message": "Successfully logged out"}

# Or clear individually
@app.post("/logout-access")
def logout_access_only(response: Response):
    """Clear only the access token cookie."""
    auth.unset_access_cookies(response)
    return {"message": "Access token cleared"}

Refreshing tokens with cookies

from fastapi import Request

@app.post("/refresh")
async def refresh_token(request: Request, response: Response):
    """Refresh access token using refresh token from cookie."""
    # Get refresh token from cookie (automatic)
    refresh_token = await auth.get_refresh_token_from_request(request)
    
    # Verify refresh token
    payload = auth.verify_token(refresh_token, verify_type=True)
    
    # Create new access token
    new_access_token = auth.create_access_token(uid=payload.sub)
    
    # Set new access token in cookie
    auth.set_access_cookies(new_access_token, response)
    
    return {"message": "Token refreshed"}

Dual token location strategy

Use cookies for refresh tokens and headers for access tokens:
auth_config = AuthXConfig(
    JWT_ALGORITHM="HS256",
    JWT_SECRET_KEY="your-secret-key",
    # Accept tokens from both locations
    JWT_TOKEN_LOCATION=["headers", "cookies"],
    # Cookie settings for refresh tokens
    JWT_COOKIE_SECURE=False,
    JWT_COOKIE_HTTP_ONLY=True,
    JWT_COOKIE_SAMESITE="lax",
    JWT_COOKIE_CSRF_PROTECT=True,
)

auth = AuthX(config=auth_config)

@app.post("/login")
def login(data: LoginRequest, response: Response):
    """Login: return access token in body, set refresh token in cookie."""
    if data.username in USERS and USERS[data.username]["password"] == data.password:
        # Access token in response body (client stores in memory)
        access_token = auth.create_access_token(uid=data.username)
        
        # Refresh token in HTTP-only cookie (secure storage)
        refresh_token = auth.create_refresh_token(uid=data.username)
        auth.set_refresh_cookies(refresh_token, response)
        
        return {
            "access_token": access_token,
            "token_type": "bearer"
        }
    
    raise HTTPException(status_code=401, detail="Invalid credentials")

@app.get("/protected")
async def protected_route(request: Request):
    """Protected route: access token from Authorization header."""
    # Get access token from headers only
    access_token = await auth.get_access_token_from_request(
        request,
        locations=["headers"]
    )
    payload = auth.verify_token(access_token, verify_csrf=False)
    
    return {"message": "Access granted", "user": payload.sub}

@app.post("/refresh")
async def refresh_token(request: Request):
    """Refresh: get refresh token from cookie, return new access token."""
    # Get refresh token from cookies only
    refresh_token = await auth.get_refresh_token_from_request(
        request,
        locations=["cookies"]
    )
    payload = auth.verify_token(refresh_token, verify_type=True)
    
    # Return new access token in body
    new_access_token = auth.create_access_token(uid=payload.sub)
    return {"access_token": new_access_token, "token_type": "bearer"}
Best practice: Store access tokens (short-lived) in memory and refresh tokens (long-lived) in HTTP-only cookies. This provides a balance between security and convenience.

Session vs persistent cookies

Control cookie lifetime:
# Session cookie (deleted when browser closes)
auth_config = AuthXConfig(
    JWT_COOKIE_MAX_AGE=None,  # Session cookie
    JWT_SESSION_COOKIE=True,
)

# Persistent cookie (survives browser restart)
auth_config = AuthXConfig(
    JWT_COOKIE_MAX_AGE=2592000,  # 30 days in seconds
    JWT_SESSION_COOKIE=False,
)

Complete example

from datetime import timedelta
from fastapi import FastAPI, HTTPException, Request, Response
from pydantic import BaseModel
from authx import AuthX, AuthXConfig
from authx.schema import TokenPayload

app = FastAPI(title="Cookie Authentication Example")

# Configure for cookie-based auth
auth_config = AuthXConfig(
    JWT_ALGORITHM="HS256",
    JWT_SECRET_KEY="your-secret-key",
    JWT_TOKEN_LOCATION=["cookies"],
    JWT_ACCESS_TOKEN_EXPIRES=timedelta(minutes=15),
    JWT_REFRESH_TOKEN_EXPIRES=timedelta(days=30),
    # Cookie settings
    JWT_COOKIE_SECURE=False,  # Set True in production
    JWT_COOKIE_HTTP_ONLY=True,
    JWT_COOKIE_SAMESITE="lax",
    JWT_COOKIE_CSRF_PROTECT=True,
    JWT_CSRF_IN_COOKIES=True,
    JWT_CSRF_METHODS=["POST", "PUT", "PATCH", "DELETE"],
)

auth = AuthX(config=auth_config)
auth.handle_errors(app)

class LoginRequest(BaseModel):
    username: str
    password: str

USERS = {
    "user1": {"password": "password1", "email": "[email protected]"},
    "user2": {"password": "password2", "email": "[email protected]"},
}

@app.post("/login")
def login(data: LoginRequest, response: Response):
    """Login and set tokens in cookies."""
    if data.username in USERS and USERS[data.username]["password"] == data.password:
        # Create tokens
        access_token = auth.create_access_token(uid=data.username)
        refresh_token = auth.create_refresh_token(uid=data.username)
        
        # Set in HTTP-only cookies
        auth.set_access_cookies(access_token, response)
        auth.set_refresh_cookies(refresh_token, response)
        
        return {"message": "Login successful", "username": data.username}
    
    raise HTTPException(status_code=401, detail="Invalid credentials")

@app.post("/logout")
def logout(response: Response):
    """Logout and clear cookies."""
    auth.unset_cookies(response)
    return {"message": "Successfully logged out"}

@app.get("/protected")
async def protected_route(payload: TokenPayload = auth.ACCESS_REQUIRED):
    """Protected route - token from cookie."""
    return {
        "message": "Access granted",
        "username": payload.sub,
        "email": USERS.get(payload.sub, {}).get("email"),
    }

@app.post("/posts")
async def create_post(payload: TokenPayload = auth.ACCESS_REQUIRED):
    """Create post - CSRF protection automatic."""
    return {"message": "Post created", "user": payload.sub}

@app.post("/refresh")
async def refresh_token(request: Request, response: Response):
    """Refresh access token."""
    refresh_token = await auth.get_refresh_token_from_request(request)
    payload = auth.verify_token(refresh_token, verify_type=True)
    
    new_access_token = auth.create_access_token(uid=payload.sub)
    auth.set_access_cookies(new_access_token, response)
    
    return {"message": "Token refreshed"}

Client-side implementation

// Login
async function login(username, password) {
    const response = await fetch('http://localhost:8000/login', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        credentials: 'include',  // Important: include cookies
        body: JSON.stringify({username, password})
    });
    return response.json();
}

// Access protected route
async function getProtected() {
    const response = await fetch('http://localhost:8000/protected', {
        credentials: 'include'  // Include cookies
    });
    return response.json();
}

// POST with CSRF token
async function createPost(title, content) {
    // Get CSRF token from cookie
    const csrfToken = getCookie('csrf_access_token');
    
    const response = await fetch('http://localhost:8000/posts', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': csrfToken  // Required!
        },
        credentials: 'include',
        body: JSON.stringify({title, content})
    });
    return response.json();
}

function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

// Logout
async function logout() {
    const response = await fetch('http://localhost:8000/logout', {
        method: 'POST',
        credentials: 'include'
    });
    return response.json();
}

Security best practices

Cookie security checklist:
  • ✅ Always use JWT_COOKIE_SECURE=True in production (requires HTTPS)
  • ✅ Always use JWT_COOKIE_HTTP_ONLY=True to prevent XSS
  • ✅ Enable CSRF protection with JWT_COOKIE_CSRF_PROTECT=True
  • ✅ Use JWT_COOKIE_SAMESITE="lax" or "strict" for CSRF protection
  • ✅ Set appropriate JWT_COOKIE_DOMAIN for your domain
  • ✅ Include CSRF token in headers for state-changing requests
  • ✅ Use short expiration times for access tokens
  • ⚠️ Never set JWT_COOKIE_HTTP_ONLY=False for authentication tokens
  • ⚠️ Don’t disable CSRF protection in production

Next steps

CSRF protection

Deep dive into CSRF protection mechanisms

Token revocation

Implement logout with token blocklisting

Error handling

Handle cookie and CSRF errors

Access and refresh tokens

Combine cookies with refresh token flow

Build docs developers (and LLMs) love