FastAPI Authentication with Keycloak: Securing Python APIs
Last updated: March 2026
FastAPI is one of the fastest-growing Python web frameworks, and for good reason — it combines high performance with automatic OpenAPI documentation and a dependency injection system that makes auth middleware elegant to implement. Pair it with Keycloak for single sign-on and identity management, and you have a production-ready API authentication stack that doesn’t require a SaaS subscription.
This guide walks through securing a FastAPI application with Keycloak, covering JWT validation with public keys, role-based access control, token introspection, and CORS configuration. Every code example is production-oriented and uses current library versions.
Prerequisites
- Python 3.10+
- A running Keycloak instance (version 22+). Use our Docker Compose Generator for a quick local setup or try a managed Keycloak instance.
- Basic familiarity with FastAPI and OAuth 2.0 / OIDC concepts
Step 1: Set Up a Keycloak Client
In the Keycloak Admin Console:
- Go to Clients > Create client
- Set Client ID to
fastapi-app - Set Client type to
OpenID Connect - Enable Client authentication (confidential client)
- Set Valid redirect URIs to
http://localhost:8000/* - Set Web origins to
http://localhost:8000
After saving, copy the Client secret from the Credentials tab.
Create Roles
For this tutorial, create two client roles:
- Go to your client’s Roles tab
- Create
api-readandapi-adminroles - Assign them to test users under Users > Role mappings > Client roles
To learn more about configuring role-based access, read our RBAC feature overview or our detailed guide on fine-grained authorization in Keycloak.
Step 2: Install Dependencies
Create a requirements.txt:
fastapi>=0.115.0
uvicorn[standard]>=0.34.0
PyJWT[crypto]>=2.9.0
httpx>=0.28.0
pydantic>=2.10.0
pydantic-settings>=2.7.0
python-dotenv>=1.0.0
Install everything:
pip install -r requirements.txt
A note on JWT libraries: we use PyJWT rather than python-jose. PyJWT is actively maintained and widely adopted, whereas python-jose has received very few updates in recent years. PyJWT with the [crypto] extra provides the cryptography backend needed for RS256 signature verification.
Step 3: Configuration with Pydantic Settings
Create a config.py that loads Keycloak settings from environment variables:
# config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
keycloak_url: str = "http://localhost:8080"
keycloak_realm: str = "your-realm"
keycloak_client_id: str = "fastapi-app"
keycloak_client_secret: str = ""
@property
def issuer(self) -> str:
return f"{self.keycloak_url}/realms/{self.keycloak_realm}"
@property
def jwks_url(self) -> str:
return f"{self.issuer}/protocol/openid-connect/certs"
@property
def token_url(self) -> str:
return f"{self.issuer}/protocol/openid-connect/token"
@property
def introspection_url(self) -> str:
return f"{self.issuer}/protocol/openid-connect/token/introspect"
model_config = {"env_file": ".env", "env_prefix": ""}
@lru_cache
def get_settings() -> Settings:
return Settings()
Create a .env file:
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=fastapi-app
KEYCLOAK_CLIENT_SECRET=your-client-secret
The @lru_cache ensures settings are loaded once and reused. The computed properties derive OIDC endpoint URLs from the base Keycloak URL so you only need to configure two values.
Step 4: Build the JWT Validation Layer
This is the core of the authentication system. We fetch Keycloak’s public keys via the JWKS endpoint and use them to validate access tokens locally — no round-trip to Keycloak per request:
# auth.py
import jwt
from jwt import PyJWKClient
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
from typing import Optional
from config import Settings, get_settings
security = HTTPBearer()
class TokenPayload(BaseModel):
"""Parsed and validated token claims."""
sub: str
email: Optional[str] = None
preferred_username: Optional[str] = None
realm_roles: list[str] = []
client_roles: list[str] = []
scope: str = ""
class JWKSKeyManager:
"""Manages JWKS key fetching with caching."""
def __init__(self):
self._clients: dict[str, PyJWKClient] = {}
def get_client(self, jwks_url: str) -> PyJWKClient:
if jwks_url not in self._clients:
self._clients[jwks_url] = PyJWKClient(
jwks_url,
cache_keys=True,
lifespan=3600, # Cache keys for 1 hour
)
return self._clients[jwks_url]
key_manager = JWKSKeyManager()
def decode_token(
token: str,
settings: Settings,
) -> TokenPayload:
"""Decode and validate a Keycloak JWT access token."""
try:
jwks_client = key_manager.get_client(settings.jwks_url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="account",
issuer=settings.issuer,
options={
"verify_exp": True,
"verify_aud": True,
"verify_iss": True,
},
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
)
except jwt.InvalidAudienceError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token audience",
)
except jwt.PyJWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token validation failed: {str(e)}",
)
# Extract roles from Keycloak's token structure
realm_roles = payload.get("realm_access", {}).get("roles", [])
client_roles = (
payload.get("resource_access", {})
.get(settings.keycloak_client_id, {})
.get("roles", [])
)
return TokenPayload(
sub=payload["sub"],
email=payload.get("email"),
preferred_username=payload.get("preferred_username"),
realm_roles=realm_roles,
client_roles=client_roles,
scope=payload.get("scope", ""),
)
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
settings: Settings = Depends(get_settings),
) -> TokenPayload:
"""FastAPI dependency that extracts and validates the current user."""
return decode_token(credentials.credentials, settings)
Key implementation details:
- JWKS caching: The
PyJWKClientcaches public keys for one hour, so your API doesn’t fetch keys from Keycloak on every single request. Keys are refreshed automatically when a token uses an unknown key ID (which happens during key rotation). - Local validation: The token is validated entirely on your API server. This is faster and more resilient than calling Keycloak’s introspection endpoint on every request.
- Role extraction: Keycloak embeds roles in a nested structure within the JWT. We extract both realm-level roles and client-specific roles.
You can inspect the structure of your Keycloak tokens using our JWT Token Analyzer to verify the claims being sent.
Step 5: Create Role-Based Dependencies
Build reusable FastAPI dependencies for role checks:
# dependencies.py
from functools import wraps
from fastapi import Depends, HTTPException, status
from auth import TokenPayload, get_current_user
def require_roles(*required_roles: str):
"""Dependency factory that checks for required roles."""
async def role_checker(
user: TokenPayload = Depends(get_current_user),
) -> TokenPayload:
all_roles = set(user.realm_roles + user.client_roles)
missing = set(required_roles) - all_roles
if missing:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required roles: {', '.join(missing)}",
)
return user
return role_checker
def require_any_role(*allowed_roles: str):
"""Dependency factory that checks if user has any of the allowed roles."""
async def role_checker(
user: TokenPayload = Depends(get_current_user),
) -> TokenPayload:
all_roles = set(user.realm_roles + user.client_roles)
if not all_roles.intersection(allowed_roles):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return user
return role_checker
# Convenience dependencies
require_admin = require_roles("api-admin")
require_read = require_any_role("api-read", "api-admin")
This pattern leverages FastAPI’s dependency injection system cleanly. require_roles demands the user have all specified roles; require_any_role demands they have at least one.
Step 6: Token Introspection Endpoint
While local JWT validation is preferred for performance, sometimes you need to check whether a token has been revoked. Keycloak’s token introspection endpoint answers this:
# introspection.py
import httpx
from fastapi import Depends, HTTPException, status
from config import Settings, get_settings
async def introspect_token(
token: str,
settings: Settings,
) -> dict:
"""Check if a token is still active via Keycloak's introspection endpoint."""
async with httpx.AsyncClient() as client:
response = await client.post(
settings.introspection_url,
data={
"token": token,
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
"token_type_hint": "access_token",
},
)
if response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Failed to reach Keycloak introspection endpoint",
)
result = response.json()
if not result.get("active"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token is no longer active",
)
return result
Use introspection sparingly — for sensitive operations like financial transactions or admin actions where you need real-time revocation checks. For general API traffic, local JWT validation is sufficient and far more performant.
For more on token lifecycle patterns, see our post on JWT token lifecycle management.
Step 7: Build the FastAPI Application
Bring everything together in your main application:
# main.py
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel
from auth import TokenPayload, get_current_user, security
from config import get_settings
from dependencies import require_admin, require_read
from introspection import introspect_token
@asynccontextmanager
async def lifespan(app: FastAPI):
# Warm up: pre-fetch JWKS keys on startup
settings = get_settings()
from auth import key_manager
client = key_manager.get_client(settings.jwks_url)
# Force an initial key fetch
try:
client.get_jwk_set()
except Exception:
print("Warning: Could not pre-fetch JWKS keys")
yield
app = FastAPI(
title="FastAPI + Keycloak Example",
version="1.0.0",
lifespan=lifespan,
)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # React/Next.js frontend
"http://localhost:5173", # Vite dev server
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Response Models ---
class UserInfo(BaseModel):
sub: str
email: str | None
username: str | None
realm_roles: list[str]
client_roles: list[str]
class Item(BaseModel):
id: int
name: str
owner: str
# --- Public Endpoints ---
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# --- Protected Endpoints ---
@app.get("/me", response_model=UserInfo)
async def get_current_user_info(
user: TokenPayload = Depends(get_current_user),
):
"""Return the authenticated user's information."""
return UserInfo(
sub=user.sub,
email=user.email,
username=user.preferred_username,
realm_roles=user.realm_roles,
client_roles=user.client_roles,
)
@app.get("/items", response_model=list[Item])
async def list_items(
user: TokenPayload = Depends(require_read),
):
"""List items. Requires 'api-read' or 'api-admin' role."""
return [
Item(id=1, name="Widget", owner=user.sub),
Item(id=2, name="Gadget", owner=user.sub),
]
@app.post("/items", response_model=Item, status_code=201)
async def create_item(
item_name: str,
user: TokenPayload = Depends(require_admin),
):
"""Create an item. Requires 'api-admin' role."""
return Item(id=3, name=item_name, owner=user.sub)
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(
item_id: int,
user: TokenPayload = Depends(require_admin),
credentials: HTTPAuthorizationCredentials = Security(security),
):
"""
Delete an item.
Requires 'api-admin' role AND active token (introspection check).
"""
settings = get_settings()
await introspect_token(credentials.credentials, settings)
# Deletion logic here
return None
# --- Admin-Only Endpoints ---
@app.get("/admin/users")
async def list_users(
user: TokenPayload = Depends(require_admin),
):
"""Admin endpoint to list users."""
return {
"message": "Admin access granted",
"admin_user": user.preferred_username,
}
CORS Configuration
Getting CORS right is critical when your frontend and API are on different origins. The configuration above explicitly lists allowed origins. In production, restrict this to your actual frontend domains. For a detailed walkthrough of CORS settings with Keycloak, see our guide on configuring CORS with your Keycloak OIDC client.
Step 8: Run and Test
Start your API:
uvicorn main:app --reload --port 8000
Get an Access Token
Fetch a token from Keycloak using the password grant (for testing only):
TOKEN=$(curl -s -X POST
http://localhost:8080/realms/your-realm/protocol/openid-connect/token
-H "Content-Type: application/x-www-form-urlencoded"
-d "client_id=fastapi-app"
-d "client_secret=your-client-secret"
-d "grant_type=password"
-d "username=testuser"
-d "password=testpassword"
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
Call Protected Endpoints
# Get current user info
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/me
# List items (requires api-read role)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/items
# Create item (requires api-admin role)
curl -X POST -H "Authorization: Bearer $TOKEN"
"http://localhost:8000/items?item_name=NewItem"
# Admin endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/admin/users
Interactive Documentation
Visit http://localhost:8000/docs to see FastAPI’s auto-generated Swagger UI. Click the Authorize button and paste your Bearer token to test endpoints interactively.
Step 9: Add Service Account Authentication
For service-to-service communication where no user is involved, use Keycloak’s client credentials grant:
# service_auth.py
import httpx
from config import get_settings
async def get_service_token() -> str:
"""Get an access token using client credentials (service account)."""
settings = get_settings()
async with httpx.AsyncClient() as client:
response = await client.post(
settings.token_url,
data={
"client_id": settings.keycloak_client_id,
"client_secret": settings.keycloak_client_secret,
"grant_type": "client_credentials",
},
)
if response.status_code != 200:
raise RuntimeError("Failed to obtain service token from Keycloak")
return response.json()["access_token"]
Enable service accounts for your client in Keycloak: go to Clients > fastapi-app > Settings and enable Service accounts roles. Then assign the appropriate roles under the Service account roles tab.
Security Best Practices
Before going to production, review these recommendations:
-
Use short-lived access tokens. Configure Keycloak to issue access tokens with a 5-15 minute lifetime. Clients should use refresh tokens to get new access tokens silently.
-
Always validate the
issandaudclaims. The code above does this. Without issuer validation, a token from a different Keycloak realm or a different OIDC provider could be accepted. -
Enable audit logging. Track who authenticated, when, and from where. This is essential for security investigations and compliance. Our guide on auditing in Keycloak covers this in detail.
-
Enforce HTTPS in production. Never transmit tokens over unencrypted connections. If you’re running Keycloak behind a reverse proxy, see our guide on how to run Keycloak behind a reverse proxy.
-
Consider MFA for users with elevated privileges. Keycloak supports conditional MFA policies based on roles or client access.
-
Monitor session activity. Keycloak provides session management APIs that let you view and terminate active sessions.
What’s Next
With your FastAPI application secured, consider these next steps:
- Add SCIM provisioning for automated user lifecycle management. You can test your SCIM endpoints with our SCIM Endpoint Tester.
- Integrate with a React or Next.js frontend — see our companion guide on Next.js + Keycloak authentication.
- Set up identity brokering to let users log in with external providers like Google or Entra ID.
- Read the Keycloak Server Administration Guide for advanced configuration.
Don’t want to manage Keycloak infrastructure? Skycloak handles the deployment, scaling, backups, and upgrades so you can focus on building your API. Check out our pricing or explore our hosting options.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.