FastAPI makes it easy to ship a Python API in an afternoon, and just as easy to ship one where every endpoint is open, because authentication and authorisation only exist where a developer declares a dependency for them. FastAPI is now the default framework for many Python back ends, AI inference services, agent tool servers and MCP servers, so its security model decides who can call model endpoints, read data and spend API credits. This guide explains how identity and access actually work in FastAPI (security schemes as dependencies, OAuth2 scopes through Security(), JWT handling), how to structure admin and privileged routes, how to handle API keys and service accounts, what to log, a hardening baseline for production, and the misconfigurations seen most often. It is written for Python developers, API security engineers and reviewers. It assumes the general material in the API Security Guide and the JWT Security Guide, and focuses on what is specific to FastAPI.
Key takeaways
- Security is opt-in per route. A path operation is protected only if it, its router or the app declares a security dependency; a missing
Dependsis an open endpoint, not an error. - Put protection at the router or app level.
FastAPI(dependencies=[...])andAPIRouter(dependencies=[...])apply a dependency to every route beneath them, which makes "secure by default, open by exception" possible. - Scopes are documented, not enforced, by OpenAPI.
Security(..., scopes=[...])andSecurityScopestell your code which scopes are required, but your dependency must check the token and reject it. - Validate tokens fully. Pin the algorithm list in
jwt.decode, check expiry, issuer and audience, and never place scopes in a token simply because the client asked for them. - Pydantic ignores unknown fields by default. Use
extra="forbid"on input models and separate response models so that clients cannot set, or see, fields they should not.
How identity and access work in FastAPI
FastAPI has no built-in user store, session system or permission model. Instead it offers security scheme classes in fastapi.security that extract credentials from a request and register the scheme in the OpenAPI document, and a dependency injection system that runs your verification code before the route handler.
| Building block | What it does | What it does not do |
|---|---|---|
OAuth2PasswordBearer(tokenUrl="token") | Reads a bearer token from the Authorization header, returns 401 if missing, documents the flow | Validate the token, check signature, expiry or scopes |
OAuth2PasswordRequestForm | Parses username, password and requested scopes from a form post | Authenticate the user or limit which scopes they get |
HTTPBearer, HTTPBasic | Extract bearer or basic credentials | Compare them safely; for basic auth use secrets.compare_digest() on UTF-8 bytes |
APIKeyHeader, APIKeyQuery, APIKeyCookie | Extract an API key from a header, query string or cookie | Look up, hash, scope or expire the key |
Depends() | Runs a dependency and injects its result; dependencies can depend on others | Carry scope requirements |
Security() | A subclass of Depends with a scopes parameter; scopes accumulate down the dependency tree into SecurityScopes | Reject a token lacking scopes, unless your code does |
The verification dependency
The official tutorial pattern is a get_current_user dependency that decodes the JWT with PyJWT, reads the sub claim, loads the user and raises HTTPException with status 401, detail="Could not validate credentials" and headers={"WWW-Authenticate": "Bearer"} on any InvalidTokenError. Production versions should add:
- Algorithm pinning: pass an explicit list, for example
algorithms=["RS256"], and never derive it from the token header. - Issuer and audience: require
issandaudand pass expected values, so tokens for another API or tenant are rejected. - Expiry: require
exp; the tutorial uses 30 minutes for access tokens and a 15-minute default. - Unique subjects: the tutorial advises that
subbe a string unique across the application, and suggests a prefix such asusername:johndoeto avoid collisions between entity types. Use the same idea for service principals, for examplesvc:oragent:. - Key source: for tokens from an external identity provider, verify against the provider's JWKS with RS256 or ES256 (
pyjwt[crypto]) rather than a shared HS256 secret.
Scopes with Security() and SecurityScopes
Declare scopes on OAuth2PasswordBearer(scopes={...}) for documentation, then require them per route with Security(get_current_active_user, scopes=["items"]). Scopes required by every dependant accumulate: in the tutorial, /users/me/items/ needs both items and me. Inside the dependency, security_scopes.scopes lists what is required and security_scopes.scope_str fills the WWW-Authenticate: Bearer scope="..." header. The tutorial's check loops over required scopes and raises 401 "Not enough permissions" for any missing; many teams return 403 for an authenticated caller who lacks a scope, which better matches HTTP semantics. The documentation warns that its example adds the requested scopes straight into the token for simplicity, and that production code must only issue scopes the user is entitled to.
Passwords, if you must store them
The current tutorial uses pwdlib with Argon2 (PasswordHash.recommended()) and verifies a dummy hash when the username does not exist, so response times do not reveal valid usernames. Most production systems should delegate user authentication to an identity provider and keep FastAPI as a resource server. See the OAuth 2.0 and OpenID Connect Guide.
Admin roles and privileged access
Admin endpoints are where a single missing dependency does most damage, mapping directly to OWASP API5:2023 Broken Function Level Authorization.
- Separate router: mount admin routes on an
APIRouter(prefix="/admin", dependencies=[Security(require_admin, scopes=["admin"])])so that new admin routes inherit protection automatically. - Role from the identity provider: map roles or groups from validated token claims, not from request bodies or headers the client controls.
- Object-level checks in the handler or service layer: scopes say what kind of action is allowed; they do not say which record. Check ownership or tenancy for every object ID (API1:2023 Broken Object Level Authorization). See the OWASP API Top 10 API1 Broken Object Level Authorization Guide.
- Property-level control: use different Pydantic models for create, update and admin update, so fields such as
is_adminortenant_idcannot be mass-assigned (API3:2023). - Step-up for destructive actions: for deletion, key rotation or bulk export, require a fresh token or a specific high-privilege scope with a short lifetime.
- Docs are an admin surface: Swagger UI at
/docs, ReDoc at/redocand the schema at/openapi.jsonreveal every route. Disable them in production withopenapi_url=None(which also disables both UIs), or serve them only behind authentication.
Integrations, API tokens and service accounts
Many FastAPI services are called mostly by machines: other microservices, scheduled jobs, AI agents, and MCP clients. These callers are non-human identities and need the same lifecycle as user accounts. See the API Keys vs OAuth Tokens Guide.
| Caller | Recommended credential | FastAPI implementation notes |
|---|---|---|
| Internal microservice | OAuth client credentials token or mTLS identity | Validate the token in a router-level dependency; map sub or client ID to a service principal with explicit scopes. See the Machine-to-Machine OAuth (Client Credentials) Guide. |
| Partner or customer integration | Scoped API key or OAuth client | APIKeyHeader(name="X-API-Key"), look up a hashed key, compare with secrets.compare_digest(), record owner, scopes and expiry |
| AI agent or MCP client | Short-lived delegated OAuth token with narrow scopes | Treat each tool as a scoped route; log the agent identity and the user it acts for. See the Remote MCP Server Hardening Guide. |
| Outbound calls from FastAPI to LLM providers and SaaS | Secrets from a vault or environment injection | Load with Pydantic settings at startup, never return them in responses or errors, rotate on staff or supplier change |
Two practices prevent most incidents. First, never accept API keys in query strings (APIKeyQuery) for anything sensitive, because URLs end up in proxy and access logs. Second, keep the signing secret out of source: the tutorial's openssl rand -hex 32 instruction is for generating a value, and the documentation tells readers not to reuse its example key. Exposed back ends with no authentication are a recurring cause of AI-era breaches: the ShadowRay campaign abused an unauthenticated Python AI cluster API to steal cloud and AI tokens, and the McKinsey Lilli incident began with documented API endpoints that required no authentication.
Logging and detection
FastAPI does not log authentication decisions by itself. Add a middleware or a dependency that writes structured events, and keep tokens and secrets out of them.
| Event | Where to capture it | Why it matters |
|---|---|---|
| Authentication failure (401) with reason category | Security dependency | Credential stuffing and token replay (ATT&CK T1110 Brute Force, T1550.001 Application Access Token) |
| Authorisation failure (403 or scope denial) | Security dependency or service layer | Object ID enumeration and privilege probing |
| Successful privileged action | Admin router dependency | Audit trail for admin and destructive actions |
| Principal type and ID (user, service, agent) on every request | Middleware after authentication | Attribute machine and agent activity |
| Validation errors (422) by route | Exception handler for RequestValidationError | Fuzzing and injection attempts |
| Rate limit triggers and 503 from concurrency limits | Proxy, gateway or Uvicorn --limit-concurrency | Resource abuse (API4:2023), denial of service (T1499) |
Metrics
- Unprotected route count: routes with no security dependency in the resolved dependency tree, excluding an approved public list (target zero unexplained).
- Token validation failure rate: 401s per thousand requests by route and client.
- Machine credential hygiene: share of API keys and service principals with owner, scopes and expiry recorded.
- Scope breadth: share of tokens issued with admin or wildcard scopes.
- Time to revoke: minutes from a compromised key report to revocation in production.
Hardening baseline
- Default deny: declare the authentication dependency on the
FastAPIapp or everyAPIRouter, and put genuinely public routes (health, login) on a separate router. - Strict input models: set
model_config = ConfigDict(extra="forbid")on input models (Pydantic otherwise ignores extra fields), useFieldconstraints for lengths and ranges, and set aresponse_modelon every route so internal fields are filtered out. See the Input Validation and Output Encoding Guide. - Disable or protect docs:
FastAPI(openapi_url=None)in production, or protect the docs routes with a dependency. - CORS by allowlist:
CORSMiddlewaredefaults are restrictive (allow_methods=["GET"],allow_credentials=False,max_age=600). Withallow_credentials=Truenone of origins, methods or headers may be["*"], so list origins explicitly. See the CORS Misconfiguration Guide. - Host and scheme: add
TrustedHostMiddleware(allowed_hosts=[...])to reject unexpectedHostheaders with 400, and terminate TLS at a proxy or useHTTPSRedirectMiddleware. - Proxy headers only from the proxy: the server ignores
X-Forwarded-*headers unless told to trust them. Use--forwarded-allow-ips="*"only when the app is reachable solely through a trusted proxy; otherwise clients can spoof their IP and scheme. - Resource limits: set request size limits and rate limits at the proxy or gateway, and use Uvicorn options such as
--limit-concurrency(returns 503 past the limit) and--timeout-keep-alive(default 5 seconds). See the Rate Limiting and API Abuse Guide. - Error handling: return generic messages, keep stack traces out of responses, and never run with debug settings in production.
- Dependencies and supply chain: pin and scan FastAPI, Starlette, Pydantic, PyJWT and their transitive dependencies; Python package compromises such as the Ultralytics PyPI compromise show why. See the Python Security Guide.
Roll out in stages: add router-level dependencies in report-only mode (log, do not block) to find callers that would break, then enforce per router, keeping a feature flag to revert one router at a time.
Mapping to frameworks
| FastAPI control | OWASP API Top 10 2023 | OWASP ASVS 5.0 area | NIST SP 800-53 Rev 5 |
|---|---|---|---|
| Router-level security dependencies | API2, API5 | V6 Authentication, V8 Authorization | AC-3, IA-2 |
| Object ownership checks | API1 | V8 Authorization | AC-3 |
Separate input and response models, extra="forbid" | API3 | V2 Validation and Business Logic | SI-10 |
| Rate and concurrency limits | API4, API6 | V4 API and Web Service | SC-5 |
| Docs disabled, CORS and host allowlists | API8 | V13 Configuration | CM-7 |
| Route inventory from the OpenAPI schema | API9 | V13 Configuration | CM-8 |
| Key and token lifecycle | API2 | V9 Self-contained Tokens | IA-5 |
Common misconfigurations
- One route without a dependency: security is declared per path operation, a new route is added without it, and it goes live open. Router-level dependencies prevent this.
- Trusting requested scopes: copying
form_data.scopesstraight into the token, as the tutorial does for simplicity and explicitly warns against. - Weak JWT handling: HS256 with a secret committed to the repository, no audience check, or long-lived tokens with no revocation path.
- Mass assignment through shared models: one Pydantic model for create, update and read lets clients set privileged fields or see internal ones.
- Wildcard CORS and trusted proxy headers everywhere:
allow_origins=["*"]combined with cookie auth workarounds, or--forwarded-allow-ips="*"on a directly reachable server. - Public docs and schema:
/docsand/openapi.jsonleft open in production, mapping every internal and admin route for an attacker.
Practitioner checklist
- Declare authentication on the app or every router and keep an explicit public route allowlist.
- Pin JWT algorithms and validate
exp,issandaudin one shared dependency. - Issue only entitled scopes and check them with
Security()andSecurityScopes. - Enforce object-level and tenant checks for every ID in a path or body.
- Use separate Pydantic input and response models with
extra="forbid"on inputs. - Register every API key and service or agent principal with owner, scopes and expiry; compare keys in constant time.
- Disable or protect
/docs,/redocand/openapi.jsonin production. - Allowlist CORS origins and hosts; trust forwarded headers only from the proxy.
- Log authentication, authorisation and privileged events with principal type, without tokens.
- Pin and scan FastAPI, Starlette, Pydantic and PyJWT dependencies.
Standards and references
- FastAPI: OAuth2 with Password (and hashing), Bearer with JWT tokens
- FastAPI: OAuth2 scopes
- FastAPI: CORS (Cross-Origin Resource Sharing)
- FastAPI: Advanced Middleware
- OWASP: API Security Top 10 2023
- Pydantic: Models
- FastAPI: Behind a Proxy
- Uvicorn: Settings
Related NHI Mgmt Group resources: API Security Guide · JWT Security Guide · Python Security Guide · Remote MCP Server Hardening Guide · Django Security Guide