Security Guide

Table of contents

  1. Overview
  2. Authentication Schemes
    1. Swagger UI Integration
  3. OIDC Authentication
    1. Configuration
    2. Using Authentication Dependencies
      1. Strict Authentication
      2. Optional Authentication
  4. Security Headers
    1. Customizing Security Headers
  5. User Model
    1. Role-Based Access Control (RBAC)
      1. Basic Role Requirements
      2. Complex RBAC Logic
  6. JWT Token Validation
    1. Supported OIDC Providers
  7. Swagger UI Integration
  8. Rate Limiting
    1. Custom Rate Limits
  9. Best Practices
  10. Example: Complete Secure Application
  11. Troubleshooting
    1. Token Validation Fails
    2. Swagger UI Authorization Not Working
  12. Next Steps

Overview

The security module provides OIDC/OAuth2 authentication and authorization utilities for FastAPI applications.

Authentication Schemes

The library provides two authentication schemes for different use cases, both of which are automatically available in Swagger UI:

  1. AuthToken (OAuth2 Flow): A standard OAuth2 Authorization Code flow with PKCE. This is recommended for production use and is the default for automated frontend integrations.
  2. IDToken (Bearer): A standard HTTP Bearer scheme where you can manually paste a JWT (ID Token). This is useful for manual testing in Swagger UI or when you already have a token from another source.

Swagger UI Integration

The create_app() function automatically configures both schemes. In Swagger UI, you will see two authorization options:

  • AuthToken: Uses the OIDC_AUTH_URL and OIDC_TOKEN_URL to perform a full OAuth2 flow.
  • IDToken: Allows you to manually enter “Bearer ".

Both schemes are verified using the same OIDC configuration.

OIDC Authentication

Configuration

Configure your OIDC provider using environment variables:

# SSL Certificate Verification (default: True)
SSL_VERIFY=True  # Set to False for self-signed certificates in development

# Automatic configuration (recommended)
OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration

# Manual configuration
OIDC_ISSUER=https://auth.example.com/realms/organization
OIDC_JWKS_URI=https://auth.example.com/realms/organization/protocol/openid-connect/certs
OIDC_TOKEN_URL=https://auth.example.com/realms/organization/protocol/openid-connect/token
OIDC_AUTH_URL=https://auth.example.com/realms/organization/protocol/openid-connect/auth

# Client configuration
OIDC_CLIENT_ID=my-client-id
OIDC_AUDIENCE=account
SWAGGER_CLIENT_ID=my-swagger-client-id

# Token configuration
TOKEN_ALGORITHMS=RS256
OIDC_USER_NAME_CLAIM=preferred_username
OIDC_USER_ID_CLAIM=sub

Using Authentication Dependencies

Strict Authentication

Requires valid JWT token, returns 401 if missing or invalid:

from fastapi import Depends, FastAPI
from fastapi_otel_common.security import get_current_user
from fastapi_otel_common.core.models import UserBase

app = FastAPI()

@app.get("/protected")
async def protected_route(user: UserBase = Depends(get_current_user)):
    return {
        "message": "Access granted",
        "user_id": user.id,
        "email": user.email,
        "name": f"{user.given_name} {user.family_name}"
    }

Optional Authentication

Returns None if token is missing or invalid:

from typing import Optional
from fastapi import Depends
from fastapi_otel_common.security import get_current_user_optional
from fastapi_otel_common.core.models import UserBase

@app.get("/public")
async def public_route(user: Optional[UserBase] = Depends(get_current_user_optional)):
    if user:
        return {"message": f"Hello, {user.given_name}!"}
    return {"message": "Hello, anonymous!"}

Security Headers

The security headers middleware adds OWASP-recommended headers to all responses:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; ...
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()

Customizing Security Headers

To customize security headers, you can modify the middleware or override specific endpoints:

from fastapi import Response

@app.get("/custom")
async def custom_headers(response: Response):
    response.headers["X-Custom-Header"] = "value"
    return {"message": "Custom headers"}

User Model

The UserBase model represents authenticated user information:

from fastapi_otel_common.core.models import UserBase

class UserBase(BaseModel):
    id: str                    # User ID from OIDC provider
    email: str                 # User email
    given_name: str           # First name
    family_name: Optional[str] # Last name
    is_admin: bool = False    # Admin flag
    roles: Dict[str, List[str]] # Client ID to roles mapping

Role-Based Access Control (RBAC)

The library provides an RBAC system based on client roles and realm roles extracted from the JWT token.

Basic Role Requirements

Use RequireRoles for “OR” logic (at least one role required) and RequireAllRoles for “AND” logic (all roles required):

from fastapi import Depends
from fastapi_otel_common.security import RequireRoles, RequireAllRoles

# Requires 'admin' OR 'manager'
@app.get("/admin", dependencies=[Depends(RequireRoles(["admin", "manager"]))])
async def admin_area():
    return {"message": "Welcome"}

# Requires BOTH 'admin' AND 'auditor'
@app.get("/super-secret", dependencies=[Depends(RequireAllRoles(["admin", "auditor"]))])
async def secret_area():
    return {"message": "Welcome, super admin"}

Complex RBAC Logic

The library supports complex boolean logic (AND/OR/Nested) using RequireRolesComplex:

from fastapi_otel_common.security import (
    RequireRolesComplex, AnyRole, AllRoles, AnyCondition, AllConditions
)
complex_condition = AnyCondition(
    AllRoles(["admin", "auditor"]),
    AnyRole(["superadmin"])
)

@app.delete("/sensitive-data")
async def delete_data(user = Depends(RequireRolesComplex(complex_condition))):
    return {"status": "deleted"}

For more details, see the Role-Based Access Control guide.

JWT Token Validation

The library automatically validates JWT tokens:

  1. Fetches JWKS from the OIDC provider
  2. Validates token signature
  3. Checks issuer and audience
  4. Extracts user claims
  5. Returns structured user information

Supported OIDC Providers

  • Keycloak
  • Auth0
  • Okta
  • Azure AD
  • Any standard OIDC-compliant provider

Swagger UI Integration

The create_app() function automatically configures Swagger UI for OIDC:

from fastapi_otel_common import create_app

app = create_app(
    title="My API",
    swagger_ui_init_oauth={
        "usePkceWithAuthorizationCodeGrant": True,
        "clientId": "my-swagger-client-id",
        "scopes": "openid profile email api:read api:write"
    }
)

Access Swagger UI at /docs and authenticate using the “Authorize” button.

Rate Limiting

Protect your API from abuse with rate limiting:

ENABLE_RATE_LIMIT_MIDDLEWARE=True
RATE_LIMIT_PER_MINUTE=60
RATE_LIMIT_PER_HOUR=1000

Rate limiting is applied per IP address. Health check endpoints (/health, /docs) are excluded.

Custom Rate Limits

Apply custom limits to specific routes:

from fastapi_otel_common import create_app

app = create_app()
limiter = app.state.limiter

@app.get("/expensive")
@limiter.limit("5/minute")
async def expensive_endpoint(request: Request):
    # Heavy operation
    return {"status": "ok"}

Best Practices

  1. Use HTTPS in production - Never send tokens over HTTP
  2. Always verify SSL certificates - Keep SSL_VERIFY=True in production
  3. Rotate secrets regularly - Update client secrets periodically
  4. Limit token scope - Request only necessary scopes
  5. Enable rate limiting - Protect against abuse
  6. Monitor authentication - Track failed login attempts
  7. Use short-lived tokens - Configure appropriate token lifetimes
  8. Validate audience - Ensure tokens are intended for your service

Example: Complete Secure Application

from fastapi import Depends, FastAPI
from fastapi_otel_common import create_app
from fastapi_otel_common.security import get_current_user
from fastapi_otel_common.core.models import UserBase

# Create app with security and automatic OpenTelemetry instrumentation
app = create_app(
    title="Secure API",
    version="1.0.0"
)

@app.get("/")
async def root():
    return {"message": "Public endpoint"}

@app.get("/profile")
async def get_profile(user: UserBase = Depends(get_current_user)):
    return {
        "user_id": user.id,
        "email": user.email,
        "name": f"{user.given_name} {user.family_name}"
    }

@app.get("/admin")
async def admin_only(user: UserBase = Depends(get_current_user)):
    if not user.is_admin:
        raise HTTPException(status_code=403, detail="Admin access required")
    return {"message": "Admin access granted"}

Troubleshooting

Token Validation Fails

  1. Check OIDC discovery URL is accessible
  2. Verify OIDC_AUDIENCE matches your client
  3. Ensure TOKEN_ALGORITHMS is correct (usually RS256)
  4. Check token hasn’t expired

Swagger UI Authorization Not Working

  1. Verify SWAGGER_CLIENT_ID is set correctly
  2. Check redirect URL is allowed in OIDC provider
  3. Ensure scopes are configured properly

Next Steps