Security Fundamentals
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against FastAPI 0.142.2 (released 2026-09-30), Pydantic 2.13 and Starlette 1.7, checked on
2026-10-02. The code was executed on Python 3.13 with TestClient. It is the foundation of the security pages: it
explains what FastAPI’s security utilities do and do not do, and then builds the simple schemes (HTTP Basic, API
keys, HTTP Bearer) on top of them. The OAuth2 password flow with JWT, and scopes and authorization models, have
their own pages: OAuth2 Password Flow & JWT and
OAuth2 Scopes & Authorization. The protocol theory
(grants, tokens, OpenID Connect) is not repeated here; it lives in the
OAuth Reference.
Authentication vs Authorization
The two words are often used interchangeably, and the confusion causes real bugs.
| Authentication (authn) | Authorization (authz) | |
|---|---|---|
Question |
Who are you? |
What may you do? |
Result |
An identity: a user, a service account, or nobody. |
A decision: allow or deny this action on this resource. |
Failure status |
|
|
In FastAPI |
A dependency that uses a class from |
Further dependencies that receive the authenticated user and check roles, scopes or ownership. |
FastAPI does not implement either for you. The classes in fastapi.security do exactly two things: they extract
credentials from the request (the Authorization header, or a key in a header, query string or cookie) and they
declare a security scheme in the generated OpenAPI document, which makes the interactive docs show an Authorize
button. Checking that the credentials are valid, looking up the user, hashing passwords and deciding what the user
may do is your code. A route that depends on HTTPBearer() accepts any token whatsoever until your own dependency
validates it; the class alone secures nothing.
The pattern used throughout this section is a chain of small dependencies (see Dependency Injection):
-
a scheme dependency extracts the raw credentials;
-
an authentication dependency validates them and returns the user, or raises
401; -
authorization dependencies take that user and raise
403when the user may not perform the action.
The OAuth Reference covers the same split from the protocol side in Getting Started with OAuth and Scopes, Claims and Permissions.
HTTPS First
Every scheme on this page sends a secret with each request. A Basic header is only Base64-encoded (an encoding, not encryption), and a bearer token or API key is a password for whoever holds it. Without TLS anyone on the network path can read and replay them. So:
-
Serve the API over HTTPS everywhere outside local development. The official documentation treats HTTPS as something the outside world provides: a TLS termination proxy (Traefik, Nginx, a cloud load balancer) holds the certificate and forwards plain HTTP to the application. See Middleware, CORS & Proxies for running behind a proxy and Deployment for servers and containers.
-
Do not put credentials where they are logged by default. A query string such as
?api_key=…lands in access logs, proxy logs, browser history andRefererheaders; prefer a header. -
If a cookie carries a credential, set it with the
SecureandHttpOnlyattributes; see Headers & Cookies. -
The best-practice checklist for tokens (short lifetimes, storage, rotation) is in Security Best Practices.
Source: FastAPI: About HTTPS
The OpenAPI Security Schemes and the fastapi.security Classes
OpenAPI 3 has four types of security scheme, and FastAPI has a class for each. The classes are callable
dependencies: you wrap them in Depends() (or Security(), covered on the scopes page), FastAPI runs them, and
registers the scheme under components.securitySchemes.
OpenAPI type |
FastAPI class | What it extracts | Declared as |
|---|---|---|---|
|
|
A key from a named header, query parameter or cookie. |
|
|
|
The |
|
|
|
The bearer token from the |
|
|
|
The bearer token, plus the URL of the provider’s discovery document. |
|
OAuth2PasswordRequestForm, OAuth2PasswordRequestFormStrict, SecurityScopes, HTTPBasicCredentials and
HTTPAuthorizationCredentials are data holders, not schemes. Details that apply to the classes:
-
Every class accepts
scheme_name(the key undersecuritySchemes, defaulting to the class name, which matters when you use two instances of the same class),description(shown in the docs) andauto_error(defaultTrue; see below). -
HTTPBasicalso takesrealm;HTTPBearertakesbearerFormat, a hint such asJWTthat is shown in the docs;OAuth2PasswordBearertakestokenUrl,scopesandrefreshUrl;OAuth2AuthorizationCodeBearertakesauthorizationUrl,tokenUrl,refreshUrlandscopes;OpenIdConnecttakesopenIdConnectUrl. -
HTTPDigestextracts the digest header and declares the scheme, but, like every class here, verifies nothing. Digest authentication is rarely used in new APIs; prefer Bearer. -
The OAuth2 classes are the topic of the following pages. For the grants themselves see Choosing an OAuth Flow, and for OpenID Connect see OpenID Connect.
The following app declares one scheme of each family on a tiny route each and is used to print what reaches OpenAPI and what each class answers when credentials are missing:
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import (
APIKeyHeader,
HTTPBasic,
HTTPBearer,
HTTPDigest,
OAuth2AuthorizationCodeBearer,
OAuth2PasswordBearer,
OpenIdConnect,
)
app = FastAPI()
schemes = {
"basic": HTTPBasic(),
"bearer": HTTPBearer(),
"digest": HTTPDigest(),
"apikey": APIKeyHeader(name="X-API-Key"),
"password": OAuth2PasswordBearer(tokenUrl="token"),
"authcode": OAuth2AuthorizationCodeBearer(
authorizationUrl="https://idp.example.test/authorize",
tokenUrl="https://idp.example.test/token",
),
"oidc": OpenIdConnect(
openIdConnectUrl="https://idp.example.test/.well-known/openid-configuration"
),
}
for name, scheme in schemes.items():
# One tiny route per class, just to show what each one declares in OpenAPI.
def endpoint(credentials: Annotated[object, Depends(scheme)]):
return {"ok": True}
app.get(f"/{name}")(endpoint)
Source: FastAPI: Security Tools
The generated components.securitySchemes, taken from a real run:
{
"HTTPBasic": {"type": "http", "scheme": "basic"},
"HTTPBearer": {"type": "http", "scheme": "bearer"},
"HTTPDigest": {"type": "http", "scheme": "digest"},
"APIKeyHeader": {"type": "apiKey", "in": "header", "name": "X-API-Key"},
"OAuth2PasswordBearer": {"type": "oauth2", "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}},
"OAuth2AuthorizationCodeBearer": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"scopes": {},
"authorizationUrl": "https://idp.example.test/authorize",
"tokenUrl": "https://idp.example.test/token"
}
}
},
"OpenIdConnect": {
"type": "openIdConnect",
"openIdConnectUrl": "https://idp.example.test/.well-known/openid-configuration"
}
}
Source: FastAPI: Security - First Steps
Calling each route without credentials returns 401 with a WWW-Authenticate header naming the scheme (since
0.122.0, see the 401 vs 403 section):
| Class | WWW-Authenticate |
|---|---|
|
|
|
|
|
|
|
|
What the declaration does on the operation is as important as the scheme itself: every route that depends on a
scheme gets a security requirement (for example [{"HTTPBasic": []}]), and Swagger UI uses it to show the padlock
and to send the credentials you entered in the Authorize dialog. The route is documented as protected even though
the enforcement is entirely your dependency. See
OpenAPI & Interactive Docs for the rest of the document.
The auto_error Option and Optional Authentication
By default each class raises an HTTPException itself when its credentials are absent (or, for Basic and Bearer,
malformed). Passing auto_error=False turns that off: the dependency returns None instead, and the decision moves
to your code. That is how you build:
-
optional authentication: anonymous callers see the public catalogue, authenticated ones also see their own reviews;
-
alternatives: accept a bearer token or an API key or a cookie; the examples below combine several schemes, and OpenAPI lists them as alternatives.
HTTP Basic
HTTP Basic authentication (RFC 7617) sends Authorization: Basic <base64(username:password)> with every request.
It is simple, supported by every HTTP client and by browsers, which show a native login dialog when they receive
401 with WWW-Authenticate: Basic. It is appropriate for internal tools, admin endpoints behind a VPN, and
machine-to-machine calls over HTTPS, and not for end-user logins (no logout, no expiry, the password travels on
every request).
HTTPBasic parses the header and gives you HTTPBasicCredentials with username and password. Your code
compares them with the expected values. Two rules matter:
-
Compare in constant time. Python’s
==on strings stops at the first differing character, so the response time leaks how much of a guess was correct. An attacker who can measure it (a timing attack) can recover a secret one character at a time, in principle.secrets.compare_digest(a, b)takes the same time regardless of where the inputs differ. -
Encode to bytes first.
compare_digestacceptsstronly when both are ASCII; a username with a non-ASCII character raisesTypeError: comparing strings with non-ASCII characters is not supported, which would surface as a500. Encoding both sides with.encode("utf-8")removes the problem. Evaluate both comparisons before combining the results (user_okandpassword_okabove) so a wrong username does not skip the password check and reveal which part was wrong.
The expected values come from the environment (see
Settings, Lifespan & Background Tasks for
pydantic-settings); the example fails closed when they are unset, because an empty expected value is never
accepted.
import os
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
security = HTTPBasic(realm="bookshelf-admin")
# In a real app these come from settings / a secret store, never from source code.
ADMIN_USER = os.environ.get("BOOKSHELF_ADMIN_USER", "")
ADMIN_PASSWORD = os.environ.get("BOOKSHELF_ADMIN_PASSWORD", "")
def get_admin(credentials: Annotated[HTTPBasicCredentials, Depends(security)]) -> str:
user_ok = secrets.compare_digest(
credentials.username.encode("utf-8"), ADMIN_USER.encode("utf-8")
)
password_ok = secrets.compare_digest(
credentials.password.encode("utf-8"), ADMIN_PASSWORD.encode("utf-8")
)
if not (ADMIN_USER and ADMIN_PASSWORD and user_ok and password_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": 'Basic realm="bookshelf-admin"'},
)
return credentials.username
AdminDep = Annotated[str, Depends(get_admin)]
@app.get("/admin/stats")
def stats(admin: AdminDep):
return {"admin": admin, "books": 42}
Source: FastAPI: HTTP Basic Auth
Calling it, with the secrets in shell variables rather than in the command:
export BOOKSHELF_ADMIN_USER=admin
read -rs BOOKSHELF_ADMIN_PASSWORD && export BOOKSHELF_ADMIN_PASSWORD # typed, not stored in history
curl -i http://127.0.0.1:8000/admin/stats
curl -i -u "$BOOKSHELF_ADMIN_USER:$BOOKSHELF_ADMIN_PASSWORD" http://127.0.0.1:8000/admin/stats
Without credentials the response is 401 with the header WWW-Authenticate: Basic realm="bookshelf-admin", which
makes a browser prompt for a login; with valid credentials it is:
{"admin": "admin", "books": 42}
Observations from running the example:
-
A request with no header and a request with a wrong password both answer
401with the sameWWW-Authenticateheader. The first has the detailNot authenticated(raised byHTTPBasicitself), the secondIncorrect username or password(raised by the example). Use one detail message for every failed credential check, and never say which of the two fields was wrong. -
The
401response must carryWWW-Authenticate. Without it the response is not valid per RFC 9110 and browsers do not show their login dialog. When you raise the exception yourself, add the header as shown. -
realm="bookshelf-admin"is the optional protection-space name; it only labels the browser prompt. Therealmargument ofHTTPBasicsets it for the built-in error. -
The security scheme in OpenAPI is
{"type": "http", "scheme": "basic"}and the operation getssecurity: [{"HTTPBasic": []}]. Swagger UI shows a username and password form in the Authorize dialog. -
HTTP Basic has no logout and the browser keeps resending the credentials for the session. A store of users with hashed passwords, and a login endpoint that returns a token, are covered in OAuth2 Password Flow & JWT.
Tutorials that compare with == and hard-code the credentials show two things to avoid; read secrets from settings
and use compare_digest.
API Keys in a Header, Query String or Cookie
An API key is a long random string that identifies a calling application. It is the simplest scheme for server to server integrations and public APIs with per-client quotas. FastAPI has one class per location:
| Class | Reads | Notes |
|---|---|---|
|
A request header. |
The usual choice: not logged by default and not stored in browser history. The name is case-insensitive. |
|
A query parameter. |
Convenient for quick scripts and links, and the least safe: URLs appear in logs, history and |
|
A cookie. |
Useful when the caller is a browser that you want to authenticate without JavaScript touching the token. Cookie-based credentials are sent automatically by browsers, so they need CSRF protection and a CORS policy; see the middleware page. |
Guidelines for the keys themselves, which the framework does not enforce:
-
generate them with
secrets.token_urlsafe(32)(a cryptographically secure source) and show them to their owner once; -
store only a hash, as the example does with SHA-256 (the key is long and random, so a fast hash is enough, unlike passwords; see the password page for slow hashes), and compare hashes with
secrets.compare_digest; -
give each key an owner, scopes and an expiry or revocation flag, so a leaked key can be disabled;
-
load secrets from the environment or a secret manager, never from source control.
The example accepts the key from any of the three places, with auto_error=False so that the three classes become
alternatives, and raises one 401 when none matched. Security() is used instead of Depends() for the scheme
dependencies because it is the same mechanism with scope support (explained on the scopes page); for an API key
without scopes, Depends() works the same way.
import hashlib
import os
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import APIKeyCookie, APIKeyHeader, APIKeyQuery
app = FastAPI(title="Bookshelf API")
# Store only a hash of each key. The real value is shown to its owner once, when it is created.
def digest(key: str) -> bytes:
return hashlib.sha256(key.encode("utf-8")).digest()
KEYS = {digest(os.environ.get("BOOKSHELF_DEMO_KEY", "")): "reporting-service"}
header_scheme = APIKeyHeader(name="X-API-Key", scheme_name="ApiKeyHeader", auto_error=False)
query_scheme = APIKeyQuery(name="api_key", scheme_name="ApiKeyQuery", auto_error=False)
cookie_scheme = APIKeyCookie(name="bookshelf_key", scheme_name="ApiKeyCookie", auto_error=False)
def get_client(
from_header: Annotated[str | None, Security(header_scheme)],
from_query: Annotated[str | None, Security(query_scheme)],
from_cookie: Annotated[str | None, Security(cookie_scheme)],
) -> str:
key = from_header or from_query or from_cookie
if key:
candidate = digest(key)
for known, client in KEYS.items():
if secrets.compare_digest(candidate, known):
return client
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or missing API key",
headers={"WWW-Authenticate": "ApiKey"},
)
ClientDep = Annotated[str, Depends(get_client)]
@app.get("/reports/books")
def books_report(client: ClientDep):
return {"client": client, "books": 42}
Source: FastAPI: Security Tools
export BOOKSHELF_DEMO_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
curl -i -H "X-API-Key: $BOOKSHELF_DEMO_KEY" http://127.0.0.1:8000/reports/books
curl -i "http://127.0.0.1:8000/reports/books?api_key=$BOOKSHELF_DEMO_KEY"
curl -i --cookie "bookshelf_key=$BOOKSHELF_DEMO_KEY" http://127.0.0.1:8000/reports/books
All three return the same 200 JSON; a wrong or missing key returns 401 with WWW-Authenticate: ApiKey:
{"client": "reporting-service", "books": 42}
What reaches OpenAPI: one scheme per instance (scheme_name keeps them apart; without it the names would be
APIKeyHeader, APIKeyQuery and APIKeyCookie), and the operation lists them as alternatives, any one of which
is enough:
[{"ApiKeyHeader": []}, {"ApiKeyQuery": []}, {"ApiKeyCookie": []}]
Run on its own, with the default auto_error=True, each class answers a missing key with 401, the detail
Not authenticated and WWW-Authenticate: APIKey.
HTTP Bearer
HTTPBearer reads Authorization: Bearer <token> (RFC 6750) and returns HTTPAuthorizationCredentials with
scheme and credentials. It does not care what the token is: an opaque random string, a JWT, a token issued by
an identity provider. Use it when the token is obtained somewhere else, or when you do not need the OAuth2 password
form in Swagger UI. (OAuth2PasswordBearer, used on the next page, extracts the same header and additionally
tells Swagger UI where to obtain a token.) The Authorization dialog for HTTPBearer shows a single token field.
The example validates an opaque token by hashing it and comparing against a stored hash, then returns the owner. Replace the lookup with a database query, or with JWT verification (see the next page and the JWT and JOSE page); opaque-token concerns such as introspection and revocation are in Opaque Tokens, Introspection and Revocation.
import hashlib
import os
import secrets
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
app = FastAPI()
bearer = HTTPBearer(bearerFormat="opaque", description="Opaque access token issued by the Bookshelf API")
# Opaque tokens are looked up (here: compared against one hashed demo value); a JWT would be verified instead.
DEMO_TOKEN_HASH = hashlib.sha256(os.environ.get("BOOKSHELF_DEMO_TOKEN", "").encode()).digest()
def get_token_owner(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer)],
) -> str:
candidate = hashlib.sha256(credentials.credentials.encode()).digest()
if not secrets.compare_digest(candidate, DEMO_TOKEN_HASH):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
headers={"WWW-Authenticate": 'Bearer error="invalid_token"'},
)
return "alice"
@app.get("/me")
def read_me(owner: Annotated[str, Depends(get_token_owner)]):
return {"user": owner}
Source: FastAPI: Security Tools
curl -i http://127.0.0.1:8000/me
curl -i -H "Authorization: Bearer $BOOKSHELF_DEMO_TOKEN" http://127.0.0.1:8000/me
The first call returns 401 with WWW-Authenticate: Bearer and {"detail": "Not authenticated"}. A wrong token is
rejected by the example’s own check with WWW-Authenticate: Bearer error="invalid_token", which is the form RFC
6750 defines for a token that is present and invalid. A valid token returns:
{"user": "alice"}
A request whose Authorization header uses another scheme (Basic …) is treated like a missing header by
HTTPBearer: the scheme must be Bearer, compared case-insensitively. The security scheme in OpenAPI is
{"type": "http", "scheme": "bearer", "bearerFormat": "opaque", "description": …}; bearerFormat is only
documentation.
Combining Schemes and Protecting Whole Routers
Two techniques complete the picture. First, accept more than one scheme by declaring each with
auto_error=False and checking which credentials arrived, as above. The WWW-Authenticate value for the final
401 is up to you; the example lists both challenges. Second, protect many routes at once instead of adding the
dependency to every function:
-
@app.get(…, dependencies=[Depends(authenticate)])for one route; -
APIRouter(dependencies=[Depends(authenticate)])for a router (used here for everything under/internal); -
FastAPI(dependencies=[Depends(authenticate)])for the whole application, which then needs explicit exceptions for public routes such as/healthand the docs, so a router is usually the better unit.
When the route does not need the result, dependencies=[…] runs the dependency for its effect only. All three
levels are explained in Dependency Injection.
import os
import secrets
from typing import Annotated
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
from fastapi.security import (
HTTPAuthorizationCredentials,
HTTPBasic,
HTTPBasicCredentials,
HTTPBearer,
)
basic = HTTPBasic(auto_error=False)
bearer = HTTPBearer(auto_error=False)
SERVICE_USER = os.environ.get("BOOKSHELF_SERVICE_USER", "")
SERVICE_PASSWORD = os.environ.get("BOOKSHELF_SERVICE_PASSWORD", "")
SERVICE_TOKEN = os.environ.get("BOOKSHELF_SERVICE_TOKEN", "")
def _same(a: str, b: str) -> bool:
return bool(b) and secrets.compare_digest(a.encode(), b.encode())
def authenticate(
basic_credentials: Annotated[HTTPBasicCredentials | None, Security(basic)],
bearer_credentials: Annotated[HTTPAuthorizationCredentials | None, Security(bearer)],
) -> str:
if bearer_credentials and _same(bearer_credentials.credentials, SERVICE_TOKEN):
return "token-client"
if (
basic_credentials
and _same(basic_credentials.username, SERVICE_USER)
and _same(basic_credentials.password, SERVICE_PASSWORD)
):
return basic_credentials.username
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": 'Bearer, Basic realm="bookshelf"'},
)
# Every route on this router needs a caller; the return value is not used.
private = APIRouter(prefix="/internal", dependencies=[Depends(authenticate)])
@private.get("/health-detail")
def health_detail():
return {"db": "ok", "queue": "ok"}
app = FastAPI()
app.include_router(private)
@app.get("/health") # public
def health():
return {"status": "ok"}
Source: FastAPI: Global Dependencies
Results of the run: /health is public and returns 200; /internal/health-detail returns 401 without
credentials or with a wrong bearer token, and 200 with either the correct Basic credentials or the correct bearer
token. The scheme declarations in OpenAPI are:
{"HTTPBasic": {"type": "http", "scheme": "basic"}, "HTTPBearer": {"type": "http", "scheme": "bearer"}}
The protected operation gets security: [{"HTTPBasic": []}, {"HTTPBearer": []}]; the public one has none.
401 vs 403, and the Change in 0.122
The correct statuses are:
-
401 Unauthorized: the request has no valid authentication. The response must carryWWW-Authenticatenaming the scheme the client should use (RFC 9110). -
403 Forbidden: the caller is authenticated, but is not allowed to do this. Retrying with the same credentials will not help.
FastAPI’s security classes historically returned 403 Forbidden with the detail Not authenticated when
credentials were missing, which is the wrong status for "unauthenticated" and carried no WWW-Authenticate.
Since 0.122.0 (2025-11-24) the classes return 401 Unauthorized and a WWW-Authenticate header, as the previous
sections showed. Consequences:
-
Clients and tests written for older versions that assert
403for a missing token now fail on upgrade. Update them to401(the better fix), or restore the old status as shown below. -
403remains correct for authorization failures that your dependencies raise (wrong role, missing scope). -
Browsers can now react to
WWW-Authenticate: Basicby showing their login dialog for routes usingHTTPBasic.
If clients really depend on 403, override make_not_authenticated_error in a subclass. The method returns the
exception (it does not raise it; the class raises what the method returns), and every class in fastapi.security
has it. The restore example from the official documentation:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
# Return the exception; the framework raises it.
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"token_length": len(credentials.credentials)}
Running it, a request without the header returns 403 with {"detail": "Not authenticated"} and no
WWW-Authenticate header, exactly the pre-0.122 behaviour, while a request with Authorization: Bearer <anything>
passes the class (validating the token is still your dependency’s job). The default implementation, which you are
replacing, builds a 401 with WWW-Authenticate from make_authenticate_headers().
A subclass per scheme is enough; repeat the pattern for HTTPBasic403(HTTPBasic), APIKeyHeader403(APIKeyHeader) and
so on. Because the status is a property of the class, any test that overrides or constructs the scheme must use the
same subclass; see Testing.
Third-Party Options
You do not have to write all of this. These community packages build on FastAPI’s security utilities; they are listed here for orientation and are not documented or verified on this page, so read their documentation and check that they support the FastAPI version you pin:
| Package | What it provides |
|---|---|
Ready-made registration, login, password reset and email verification, with pluggable backends (cookie, JWT bearer) and database adapters. |
|
JWT-based authentication with access and refresh tokens, token storage in headers or cookies, and scopes. |
|
"Sign in with" social login (OAuth2 and OpenID Connect providers such as Google and GitHub) as FastAPI route helpers. |
For social login and federation theory see Social Login and Federation. When a managed identity provider (Keycloak, a cloud IdP) issues the tokens, FastAPI only needs to validate them, which the scopes page shows.
Where Older Guides Are Outdated, and What Changed Recently
-
403 for missing credentials (changed in 0.122.0, 2025-11-24). Older tutorials and book-era FastAPI returned
403 Forbiddenfor a missingAuthorizationheader. Current FastAPI returns401withWWW-Authenticate. Restore the old status by overridingmake_not_authenticated_erroras shown above. -
Hard-coded credentials and
==comparison. Basic-auth examples that hard-code a username and password and compare with==are unsafe; read secrets from settings and compare withsecrets.compare_digest. -
Depends(oauth2_scheme)withoutAnnotated. Material from before 0.95.0 writestoken: str = Depends(oauth2_scheme); the current style istoken: Annotated[str, Depends(oauth2_scheme)]. -
Dependency results are hashable (0.121.3, 2025-11-19). Not user-visible, but explains old workarounds in libraries that inspect
Depends()andSecurity(). -
Starlette 1.x. FastAPI 0.133.0 (2026-02-24) added support for Starlette 1.0.0 and later; the security classes are FastAPI’s own and unaffected.
The consolidated dated table is in What’s Changed & Migration.
Pitfalls
-
A scheme class is not a check.
Depends(HTTPBearer())only proves a header exists. Always follow it with a validating dependency, and attach that dependency to every protected route (or to the router). -
Secrets in code or in the OpenAPI document.
securitySchemesdescribes how to authenticate, not the secrets; never put keys indescription, defaults or examples. -
==for secrets. Usesecrets.compare_digeston bytes, and on hashes for stored keys. -
Distinguishing failures. Different detail messages for an unknown user and a wrong password let attackers enumerate accounts. Use one message.
-
Missing
WWW-Authenticate. A hand-raised401without the header is not valid HTTP and breaks browser prompts. -
Credentials in query strings leak through logs and
Referer; prefer headers. -
Mixing up
401and403. Unknown caller:401. Known caller, not permitted:403. -
Forgetting CORS and CSRF for cookies. Cookie credentials are sent automatically by browsers; see Middleware, CORS & Proxies.
-
No rate limiting. Basic and API-key endpoints are brute-force targets; add limits at the proxy or with middleware.
-
Public docs for a private API.
/docsand/openapi.jsonare public by default; see the OpenAPI page for disabling or protecting them.