Security¶
A velociraptor doesn't announce itself to the herd. It moves through the terrain unseen until it chooses to strike. Your API endpoints need the same discipline: identify every caller, reject the unfamiliar, and never leave the gates open because "it's just a small route."
VelociPy's security helpers are light, pluggable claws. They read credentials
from headers, query strings, cookies, or bearer tokens, hand the extracted value
to your handler, and automatically register the matching OpenAPI
securitySchemes. No heavy auth framework, no hidden globals - just a dependency
that guards the path.
What these helpers do and don't do
The built-in schemes extract credentials and return them to your code. They do not validate passwords, issue JWTs, or look users up in a database. That part is yours to implement, which keeps the framework small and your auth logic visible.
API keys¶
The simplest way to mark a route as protected is an API key. VelociPy can read it from a header, a query parameter, or a cookie.
Header keys¶
Add an X-API-Key header check to any endpoint with APIKeyHeader:
from typing import Annotated, Any
from velocipy import APIKeyHeader, Depends, VelociPy
app = VelociPy()
api_key_header = APIKeyHeader(name="X-API-Key")
@app.get("/api-key-protected")
async def api_key_protected(
api_key: Annotated[str, Depends(api_key_header)],
) -> dict[str, Any]:
return {"api_key": api_key}
When a request arrives without the header, the dependency raises
HTTPException(401) before your handler runs. When the header is present, the
value is passed straight through as api_key.
Header names are case-insensitive
APIKeyHeader(name="X-API-Key") matches x-api-key, X-API-KEY, or any
other casing the client sends.
Query and cookie keys¶
Use the same pattern for other hiding spots:
from velocipy import APIKeyCookie, APIKeyQuery
api_key_query = APIKeyQuery(name="api_key")
api_key_cookie = APIKeyCookie(name="session_token")
@app.get("/from-query")
async def from_query(key: Annotated[str, Depends(api_key_query)]):
return {"key": key}
@app.get("/from-cookie")
async def from_cookie(token: Annotated[str, Depends(api_key_cookie)]):
return {"token": token}
| Scheme | Reads from | OpenAPI in |
|---|---|---|
APIKeyHeader |
HTTP header | header |
APIKeyQuery |
Query string | query |
APIKeyCookie |
Cookie | cookie |
Optional credentials¶
Sometimes you want the credential if it exists, but you don't want to reject a
missing one. Set auto_error=False and the dependency returns None instead of
raising 401:
optional_key = APIKeyHeader(name="X-API-Key", auto_error=False)
@app.get("/maybe-protected")
async def maybe_protected(key: Annotated[str | None, Depends(optional_key)]):
if key is None:
return {"message": "Guest visitor"}
return {"message": f"Welcome, key holder {key[:4]}..."}
This is useful for public endpoints that show extra data to authenticated clients, or for hand-rolled login flows.
OAuth2 password bearer¶
For token-based auth, OAuth2PasswordBearer reads the Authorization header
and expects a Bearer <token> value:
from velocipy import OAuth2PasswordBearer, VelociPy
app = VelociPy()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
@app.get("/oauth2-protected")
async def oauth2_protected(token: Annotated[str, Depends(oauth2_scheme)]):
return {"token": token}
@app.post("/token")
async def token() -> dict[str, str]:
# In a real app: validate username/password, then issue a JWT or opaque token.
return {"access_token": "fake-token", "token_type": "bearer"}
The dependency only verifies that the header is present and well-formed. The actual token validation - signature, expiry, scopes, user lookup - belongs in your handler or in a follow-up dependency.
Never ship the example token endpoint
The /token route above returns a hard-coded string. It shows the shape of
the flow; in production, check credentials against your user store and issue
a properly signed token.
Scopes¶
You can declare OAuth2 scopes so they appear in the generated OpenAPI document:
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/token",
scopes={"items:read": "Read items", "items:write": "Create and update items"},
)
Your own scope-checking dependency can then read request.headers["authorization"]
or wrap the scheme and inspect the token payload.
Combining schemes¶
Routes can depend on more than one credential. VelociPy resolves each dependency independently, so you can layer header and bearer checks, or use a dependency that tries several locations:
from typing import Any
from velocipy import APIKeyCookie, APIKeyHeader, Depends
header_key = APIKeyHeader(name="X-API-Key", auto_error=False)
cookie_key = APIKeyCookie(name="api_key", auto_error=False)
async def any_api_key(
header: Annotated[str | None, Depends(header_key)],
cookie: Annotated[str | None, Depends(cookie_key)],
) -> str:
key = header or cookie
if key is None:
raise HTTPException(status_code=401, detail="Not authenticated")
return key
@app.get("/protected")
async def protected(key: Annotated[str, Depends(any_api_key)]) -> dict[str, Any]:
return {"key": key}
Custom security schemes¶
If VelociPy's built-in helpers don't match your terrain, subclass
SecurityBase. You only need to define:
- The OpenAPI scheme dictionary.
- A
__call__method that extracts and returns the credential, or raises 401.
from typing import Any
from velocipy import Depends, Request, SecurityBase, VelociPy
from velocipy.exceptions import HTTPException
from velocipy.status import HTTP_401_UNAUTHORIZED
class CustomSignatureAuth(SecurityBase):
def __init__(self, header_name: str = "X-Signature") -> None:
super().__init__(
scheme_name="CustomSignatureAuth",
scheme={"type": "apiKey", "in": "header", "name": header_name},
)
self.header_name = header_name
async def __call__(self, request: Request) -> str:
signature = request.headers.get(self.header_name.lower())
if not signature:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Missing signature",
)
# Verify the signature with your own logic here.
return signature
signature_auth = CustomSignatureAuth()
@app.get("/signed")
async def signed(signature: Annotated[str, Depends(signature_auth)]):
return {"signature": signature}
Because SecurityBase is recognized by the OpenAPI generator, your custom
scheme will appear in /docs without any extra configuration.
At a glance¶
| Tool | Reads from | Auto 401 | OpenAPI support |
|---|---|---|---|
APIKeyHeader |
Header | Yes (toggle with auto_error) |
apiKey in header |
APIKeyQuery |
Query string | Yes (toggle with auto_error) |
apiKey in query |
APIKeyCookie |
Cookie | Yes (toggle with auto_error) |
apiKey in cookie |
OAuth2PasswordBearer |
Authorization: Bearer ... |
Yes (toggle with auto_error) |
oauth2 password flow |
SecurityBase subclass |
Anything you implement | Up to you | Full custom scheme |
Keep secrets out of your code
Load API keys, token secrets, and signing keys from environment variables or a secrets manager. VelociPy's Config helper is a good place to centralize those values.
See also¶
examples/security.pyfor a runnable API-key and OAuth2 demo.- Dependencies for how
Dependsresolves values and caches them per request. - Config for typed configuration and secret management.