API Reference
This page documents the Python modules and how to use them in your FastAPI application.
Module Overview
| Module | Purpose |
|---|---|
sram_fastapi.auth |
Authentication and authorization |
sram_fastapi.config |
Configuration management |
Authentication Module
User
Dataclass representing an authenticated user from SRAM.
from sram_fastapi.auth import User
@dataclass
class User:
sub: str # Subject identifier (unique user ID)
email: str | None # User's email address
name: str | None # Display name
preferred_username: str | None # Username
eduperson_entitlement: list[str] | None # SRAM group memberships
voperson_external_affiliation: list[str] | None # Institutional affiliations
raw_claims: dict | None # All OIDC claims
Create from OIDC claims:
claims = {"sub": "abc123", "email": "user@example.com", "name": "Jane Doe"}
user = User.from_claims(claims)
OIDCClient
Client for SRAM OIDC authentication. Handles the OAuth2 flow.
from sram_fastapi.auth import OIDCClient
client = OIDCClient(settings)
# Redirect user to SRAM login
response = await client.authorize_redirect(request, redirect_uri)
# Handle callback after authentication
token, user = await client.handle_callback(request)
# Introspect a Bearer token (for API access)
token_info = await client.introspect_token(token_string)
Dependency Functions
FastAPI dependencies for protecting routes.
get_current_user
Requires authentication. Returns User or raises 401.
from fastapi import Depends
from sram_fastapi.auth import User, get_current_user
@app.get("/profile")
async def profile(user: User = Depends(get_current_user)):
return {"email": user.email}
get_optional_user
Returns User if authenticated, None otherwise.
from fastapi import Depends
from sram_fastapi.auth import User, get_optional_user
@app.get("/")
async def home(user: User | None = Depends(get_optional_user)):
if user:
return {"message": f"Hello, {user.name}"}
return {"message": "Hello, guest"}
get_token_user
Authenticates via Bearer token (for API/CLI access). Uses token introspection.
from fastapi import Depends
from sram_fastapi.auth import User, get_token_user
@app.get("/api/data")
async def api_data(user: User = Depends(get_token_user)):
return {"user": user.sub}
Client usage:
curl -H "Authorization: Bearer <access_token>" https://your-app/api/data
get_oidc_client
Returns the OIDCClient instance. Use in auth routes.
from fastapi import Depends, Request
from sram_fastapi.auth import OIDCClient, get_oidc_client
from sram_fastapi.config import get_settings
@app.get("/auth/login")
async def login(
request: Request,
oidc_client: OIDCClient = Depends(get_oidc_client),
):
settings = get_settings()
redirect_uri = f"{settings.base_url}/auth/callback"
return await oidc_client.authorize_redirect(request, redirect_uri)
Authorization Module
AuthorizationError
Exception raised when a user lacks required permissions.
from sram_fastapi.auth import AuthorizationError
class AuthorizationError(Exception):
required: list[str] # What was required
actual: list[str] # What the user has
check_type: str # "entitlement" or "affiliation"
require_all: bool # Whether all values were required
Handle with a custom exception handler:
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(AuthorizationError)
async def auth_error_handler(request: Request, exc: AuthorizationError):
return JSONResponse(
status_code=403,
content={
"detail": "Access denied",
"required": exc.required,
"check_type": exc.check_type,
},
)
require_entitlement
Factory that creates a dependency requiring specific SRAM group memberships.
from fastapi import Depends
from sram_fastapi.auth import User, require_entitlement
# Require membership in a specific group
@app.get("/admin")
async def admin_page(
user: User = Depends(require_entitlement(
"urn:mace:surf.nl:sram:group:myorg:myco:admins"
))
):
return {"message": "Welcome, admin"}
# Require ANY of multiple entitlements (OR logic)
@app.get("/staff")
async def staff_page(
user: User = Depends(require_entitlement(
"urn:mace:surf.nl:sram:group:myorg:myco:admins",
"urn:mace:surf.nl:sram:group:myorg:myco:staff",
))
):
return {"message": "Welcome, staff member"}
# Require ALL entitlements (AND logic)
@app.get("/superadmin")
async def superadmin_page(
user: User = Depends(require_entitlement(
"urn:mace:surf.nl:sram:group:myorg:myco:admins",
"urn:mace:surf.nl:sram:group:myorg:myco:security",
require_all=True,
))
):
return {"message": "Welcome, superadmin"}
require_affiliation
Factory that creates a dependency requiring specific institutional affiliations.
from fastapi import Depends
from sram_fastapi.auth import User, require_affiliation
# Require exact affiliation
@app.get("/tudelft-staff")
async def tudelft_staff(
user: User = Depends(require_affiliation("staff@tudelft.nl"))
):
return {"message": "Welcome, TU Delft staff"}
# Wildcard: any organization with "staff" role
@app.get("/any-staff")
async def any_staff(
user: User = Depends(require_affiliation("staff@"))
):
return {"message": "Welcome, staff member"}
# Wildcard: any role at specific organization
@app.get("/tudelft-member")
async def tudelft_member(
user: User = Depends(require_affiliation("@tudelft.nl"))
):
return {"message": "Welcome, TU Delft member"}
Affiliation pattern matching:
| Pattern | Matches |
|---|---|
staff@tudelft.nl |
Exact match only |
staff@ |
Any org with staff role (e.g., staff@tudelft.nl, staff@uu.nl) |
@tudelft.nl |
Any role at TU Delft (e.g., staff@tudelft.nl, student@tudelft.nl) |
Configuration Module
Settings
Pydantic settings class for configuration. Loads from environment variables or .env file.
from sram_fastapi.config import Settings
class Settings(BaseSettings):
# Application
app_name: str = "SRAM FastAPI"
debug: bool = False
secret_key: str = "change-me-in-production"
# SRAM OIDC (required)
sram_oidc_client_id: str
sram_oidc_client_secret: str
sram_oidc_discovery_url: str = "https://proxy.sram.surf.nl/.well-known/openid-configuration"
# Session
session_cookie_name: str = "session"
session_max_age: int = 3600 # seconds
session_https_only: bool = True
# Server
base_url: str = "http://localhost:8124"
allowed_redirect_urls: list[str] = ["http://localhost:8124"]
get_settings
Cached function returning the settings instance.
from sram_fastapi.config import get_settings
settings = get_settings()
print(settings.sram_oidc_client_id)
As a FastAPI dependency:
from fastapi import Depends
from sram_fastapi.config import Settings, get_settings
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {"app": settings.app_name}
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
SRAM_OIDC_CLIENT_ID |
Yes | - | OIDC client ID from SRAM |
SRAM_OIDC_CLIENT_SECRET |
Yes | - | OIDC client secret |
SRAM_OIDC_DISCOVERY_URL |
No | https://proxy.sram.surf.nl/.well-known/openid-configuration |
OIDC discovery endpoint |
SECRET_KEY |
Yes | - | Secret for session encryption |
BASE_URL |
No | http://localhost:8124 |
Public URL of your app |
DEBUG |
No | false |
Enable debug mode |
SESSION_MAX_AGE |
No | 3600 |
Session duration in seconds |
SESSION_HTTPS_ONLY |
No | true |
Require HTTPS for cookies |
Complete Example
Minimal FastAPI app with SRAM authentication:
from fastapi import Depends, FastAPI, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from sram_fastapi.auth import (
OIDCClient,
User,
get_current_user,
get_oidc_client,
get_optional_user,
)
from sram_fastapi.config import get_settings
app = FastAPI()
settings = get_settings()
app.add_middleware(
SessionMiddleware,
secret_key=settings.secret_key,
https_only=settings.session_https_only,
)
@app.get("/")
async def home(user: User | None = Depends(get_optional_user)):
if user:
return {"message": f"Hello, {user.name}"}
return {"message": "Please log in", "login_url": "/auth/login"}
@app.get("/auth/login")
async def login(
request: Request,
oidc_client: OIDCClient = Depends(get_oidc_client),
):
redirect_uri = f"{settings.base_url}/auth/callback"
return await oidc_client.authorize_redirect(request, redirect_uri)
@app.get("/auth/callback")
async def callback(
request: Request,
oidc_client: OIDCClient = Depends(get_oidc_client),
):
token, user = await oidc_client.handle_callback(request)
request.session["user"] = user.raw_claims
return RedirectResponse(url="/")
@app.get("/auth/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/")
@app.get("/profile")
async def profile(user: User = Depends(get_current_user)):
return {
"sub": user.sub,
"email": user.email,
"name": user.name,
"entitlements": user.eduperson_entitlement,
}