Middleware¶
The SDK provides two middleware classes. Use AuthzMiddleware in authz mode (dual-token) or JWTAuthMiddleware in proxy mode (single JWT). If you use the Sentinel class, protect() adds the correct one automatically.
AuthzMiddleware (AuthZ Mode)¶
Validates both an IdP token and a Sentinel authorization token on each request. Checks that the sub claims match across tokens (binding verification).
Headers¶
| Header | Content |
|---|---|
Authorization |
Bearer <idp_token> |
X-Authz-Token |
<sentinel_authz_token> |
Setup¶
from sentinel_auth.authz_middleware import AuthzMiddleware
app.add_middleware(
AuthzMiddleware,
idp_jwks_url="https://www.googleapis.com/oauth2/v3/certs",
sentinel_public_key=sentinel_pem,
exclude_paths=["/health", "/docs", "/openapi.json"],
)
Or pass a Sentinel instance (keys are read lazily -- safe to call before lifespan):
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
idp_public_key |
str \| None |
None |
PEM key for IdP token validation |
idp_jwks_url |
str \| None |
None |
JWKS endpoint for IdP tokens (handles key rotation) |
sentinel_public_key |
str \| None |
None |
PEM key for authz token validation |
sentinel_instance |
Sentinel \| None |
None |
Sentinel instance (reads keys lazily) |
idp_algorithm |
str |
"RS256" |
IdP token signing algorithm |
sentinel_algorithm |
str |
"RS256" |
Authz token signing algorithm |
sentinel_audience |
str |
"sentinel:authz" |
Expected aud claim in authz token |
exclude_paths |
list[str] \| None |
["/health", "/docs", "/openapi.json"] |
Paths that bypass authentication |
Either sentinel_public_key or sentinel_instance is required. For IdP validation, the middleware uses idp_jwks_url or idp_public_key (from the params or from the Sentinel instance).
Request State¶
After successful validation, the middleware sets:
| Attribute | Type | Description |
|---|---|---|
request.state.user |
AuthenticatedUser |
User built from authz token claims + IdP email/name |
request.state.token |
str |
The Sentinel authz token |
request.state.idp_token |
str |
The original IdP token |
Validation Steps¶
- Extract IdP token from
Authorization: Bearer ... - Extract authz token from
X-Authz-Token - Validate IdP token (signature, expiry) via JWKS or static key
- Validate authz token (signature, expiry, audience
sentinel:authz) - Verify binding: IdP
submust match authzidp_sub - Build
AuthenticatedUserand set onrequest.state
OPTIONS requests are passed through without validation.
JWTAuthMiddleware (Proxy Mode)¶
Validates a single Sentinel-issued JWT. Used when Sentinel handles the full OAuth flow.
Setup¶
from sentinel_auth.middleware import JWTAuthMiddleware
# Recommended: fetch key from JWKS automatically
app.add_middleware(
JWTAuthMiddleware,
base_url="https://sentinel.example.com",
)
# Alternative: static PEM key
app.add_middleware(
JWTAuthMiddleware,
public_key=Path("keys/public.pem").read_text(),
)
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
base_url |
str \| None |
None |
Sentinel URL. JWKS endpoint derived as {base_url}/.well-known/jwks.json |
public_key |
str \| None |
None |
RSA PEM key for air-gapped deployments |
jwks_url |
str \| None |
None |
Explicit JWKS URL (non-standard paths only) |
algorithm |
str |
"RS256" |
JWT signing algorithm |
audience |
str |
"sentinel:access" |
Expected aud claim |
exclude_paths |
list[str] \| None |
["/health", "/docs", "/openapi.json"] |
Paths that bypass authentication |
allowed_workspaces |
set[str] \| None |
None |
Workspace IDs permitted. None allows all |
One of base_url, jwks_url, or public_key is required. The JWKS key is fetched lazily on first request and cached.
Request State¶
| Attribute | Type | Description |
|---|---|---|
request.state.user |
AuthenticatedUser |
User built from JWT claims |
request.state.token |
str |
The raw JWT string |
Excluded Paths¶
Both middleware classes skip authentication for excluded paths. Matching uses exact match or prefix with / boundary:
# Path "/health" matches: /health, /health/ready
# Path "/docs" matches: /docs, /docs/oauth2-redirect
# Path "/docs" does NOT match: /documents
Error Responses¶
Both middleware classes return JSON errors:
| Status | Detail | When |
|---|---|---|
| 401 | Missing IdP token / Missing or invalid Authorization header |
No Authorization: Bearer header |
| 401 | Missing authz token |
No X-Authz-Token header (authz mode only) |
| 401 | IdP token expired / Token has expired |
Token exp in the past |
| 401 | Invalid IdP token / Invalid token |
Bad signature, malformed, wrong audience |
| 401 | Token binding mismatch: idp_sub does not match |
IdP sub != authz idp_sub (authz mode) |
| 401 | Invalid token claims |
Missing required claims in payload |
| 403 | Workspace not permitted for this service |
Workspace not in allowed_workspaces |
| 500 | Authentication service unavailable |
JWKS fetch failed (proxy mode) |