Skip to content
Guide

Security and Authentication

Apply Flasgo's secure defaults, host and CSRF origin checks, authentication, JWT, authorization, rate limits, and SSRF protection.

Flasgo provides secure primitives and restrictive defaults; your application still owns its data validation, authorization rules, secret management, dependency maintenance, and deployment controls.

import os
from flasgo import Flasgo
app = Flasgo(
settings={
"DEBUG": False,
"SECRET_KEY": os.environ["FLASGO_SECRET_KEY"],
"ALLOWED_HOSTS": {"api.example.com"},
"SESSION_COOKIE_SECURE": True,
"SESSION_COOKIE_HTTP_ONLY": True,
"SESSION_COOKIE_SAME_SITE": "Lax",
"CSRF_COOKIE_SECURE": True,
"CSRF_CHECK_ORIGIN": True,
"CSRF_REQUIRE_ORIGIN": True,
}
)

SECRET_KEY must contain at least 32 characters in every mode, including local development. Keep it stable, random, out of source control, and rotate it with an explicit session-invalidation plan. SESSION_COOKIE_SAME_SITE accepts only Lax, Strict, or None; None requires SESSION_COOKIE_SECURE=True.

Register a backend, then attach permissions to routes:

from flasgo import (
Flasgo,
HasScope,
IsAuthenticated,
User,
bearer_token_backend,
)
app = Flasgo()
async def validate_token(token: str) -> User | None:
record = await token_store.lookup(token)
if record is None:
return None
return User(
id=record.user_id,
is_authenticated=True,
scopes=frozenset(record.scopes),
)
app.register_auth_backend(
"bearer",
bearer_token_backend(validate_token),
)
@app.get("/admin")
@app.authorize(
IsAuthenticated(),
HasScope("admin"),
backend="bearer",
)
async def admin():
return {"ok": True}

Missing or invalid credentials return 401; an authenticated user without permission receives 403. An authentication backend that raises returns a generic 500 for HTTP routes, WebSocket upgrades, and protected docs alike, and counts as a security failure; it does not return 401, which would invite clients to retry credentials that may be valid. AllowAny, custom permission objects, and sync or async permission callables are supported.

In 0.10.0, after_request hooks also run for authentication and authorization denials, as they already did for rate-limit rejections. Review hooks that assume a handler ran or that a user is authenticated.

Authorization also works on WebSocket routes and runs before acceptance. Never treat authentication as object-level authorization: check that the current user may access the specific account, document, or tenant requested.

For a self-contained HS256 backend, install the optional helper:

Terminal window
uv add flasgo --extra jwt
import os
from flasgo import HasScope, IsAuthenticated, jwt_backend
app.register_auth_backend(
"jwtAuth",
jwt_backend(
os.environ["JWT_SECRET"], # At least 32 bytes.
issuer="https://identity.example.com",
audience="billing-api",
),
)
@app.get("/jwt-admin")
@app.authorize(
IsAuthenticated(),
HasScope("admin"),
backend="jwtAuth",
)
async def jwt_admin():
return {"ok": True}

jwt_backend() accepts HS256 only, rejects weak secrets and alg=none, and requires iss, aud, exp, iat, and sub. Use a dedicated random secret: since 0.11.0, jwt_backend() and encode_jwt() raise ValueError at configuration time for secrets that PyJWT refuses as HMAC keys, such as PEM, SSH, DER, or JWK-shaped text. Code that caught jwt.InvalidKeyError from encode_jwt() should catch ValueError instead. A correctly signed token whose exp, nbf, or iat overflows an integer receives a 401 Bearer challenge. leeway must be a finite, non-negative number of seconds; NaN and infinity raise ValueError. Scopes come from the configurable scope_claim; its name cannot collide with registered identity or lifetime claims. Each scope must be an RFC 6749 scope token without whitespace, quotes, or backslashes: encode_jwt(scopes=["read admin"]) raises ValueError instead of producing a token that grants admin, and list-form scope claims containing such values are rejected. encode_jwt() creates short-lived tokens with the matching contract; additional_claims cannot override registered claims or the configured scope_claim, but may include scope when a different scope_claim is used. Keep tokens out of query strings and prefer an external identity system when key rotation, asymmetric signing, revocation, or federation is required.

Bearer and JWT helpers attach accurate OpenAPI metadata automatically. Custom backends can declare a validated local Security Scheme Object with openapi_scheme= when registered. Authorized operations include the backend requirement and sorted HasScope(...) values. See API Documentation for OAuth2, OpenID Connect, and custom scheme examples.

Add an in-process sliding-window limit:

from flasgo import rate_limit
@app.post("/login")
@app.ratelimit(5, per=60)
async def login():
return {"ok": True}
@app.get("/reports")
@rate_limit(20, per=60, scope="reporting")
async def reports():
return {"reports": []}

Routes with the same scope share a quota, and each rule in a shared scope keeps the history it needs, so a shorter-window rule cannot erase requests a longer-window rule still counts. Supply a stable key_func to limit authenticated identities:

@app.get("/me")
@app.ratelimit(
100,
per=60,
key_func=lambda request: (
request.user.id if request.user else request.client_ip
),
)
async def me():
return {"ok": True}

For protected HTTP and WebSocket routes, default IP-based rules execute before the authentication backend so abusive requests cannot repeatedly invoke it. Custom identity-based key_func rules execute after successful authentication. Successful responses advertise the most restrictive quota across both phases in ratelimit-* and x-ratelimit-* headers, so a generous per-user limit cannot hide a nearly exhausted per-IP limit. A 429 includes Retry-After; for a shared scope it waits until enough entries expire to fall under that rule’s limit. Authentication failures are also throttled by the security-failure limiter.

The built-in limiter uses the direct ASGI client address and does not trust X-Forwarded-For. Its key-capacity bound fails closed for unseen keys while preserving active quota buckets rather than evicting live accounting under churn. Capacity denials report Retry-After from the earliest bucket expiry, in memory and in Redis. The default limiter is per process. Flasgo 0.9.0 adds RedisRateLimiter for shared decorated route quotas; see Shared Quotas and Sessions. Security-failure and per-WebSocket message throttling remain process-local, so retain aggregate protections at a trusted edge.

Boolean settings and SecurityConfig controls require actual booleans, including on later assignment. Convert environment strings explicitly; "false" is not a valid boolean setting. Session and CSRF cookie names must be non-empty ASCII HTTP tokens and fail application initialization if invalid, as does a SESSION_COOKIE_MAX_AGE that is not a positive integer.

ALLOWED_HOSTS accepts hostnames, IP addresses, and .example.com suffix patterns, which match subdomains but not the apex. Other entries, including "*", fail at startup. Host headers must be DNS names or IP literals: values containing URL delimiters or spaces, and bracketed IPv6 values with a zone ID, are rejected, and suffix patterns never match IPv6 literals. See Host and origin entries for accepted forms.

The default HTTP security path includes:

  • a strict Host allowlist;
  • signed, expiring session cookies;
  • HMAC-signed, session-bound double-submit CSRF tokens with Origin or Referer checks;
  • no-store caching unless explicitly relaxed;
  • CSP, HSTS, clickjacking, MIME-sniffing, referrer, and permissions headers;
  • bounded request bodies, request heads, full-body read time, multipart parts, form fields, and typed validation work;
  • throttling and logging for repeated host and CSRF failures.

Keep security headers centralized in SECURITY_HEADERS. If a page needs scripts, use a narrow nonce or hash policy rather than adding broad unsafe-inline or wildcard sources.

CSRF tokens rotate when signed session state changes, and legacy unsigned or attacker-fixed values are rejected. Publicly cacheable responses do not emit framework-managed session or CSRF cookies. Typed JSON and form models reject unknown fields and use shared depth, work, and issue budgets; validation responses do not reflect rejected values by default. Multipart parsing and form/query field counts have separate caps through MAX_MULTIPART_PARTS and MAX_FORM_FIELDS; WebSocket.query_params applies the same MAX_FORM_FIELDS limit. Numeric query, header, and form values must be plain ASCII decimals, so "1_000", " 12 ", and non-ASCII digits are rejected.

CORS is disabled by default and is separate from CSRF and authorization. Enabling an exact browser origin only controls whether JavaScript can read a response; it does not make cross-origin writes trusted. Keep CSRF enabled and configure CSRF_TRUSTED_ORIGINS independently when a cross-origin write is intentional. Entries may be exact origins (https://partner.example), scheme-qualified wildcards (https://*.example.com), bare hosts (partner.example), or bare suffixes (.example.com). Origins are compared in canonical form, and bare entries trust only the request’s own scheme, so an HTTPS application never trusts http://evil.example.com. Unsupported entries, such as chrome-extension:// origins or values with a path, fail at startup. See HTTP Applications for route-aware policies and preflight behavior.

Duplicate session-cookie values are treated as an absent credential. Repeated CSRF cookies or token headers fail unsafe-request validation; origin checking rejects repeated Origin or Referer fields even when one value is valid. Cookie values and raw Set-Cookie headers reject all C0 controls and DEL. Failed dispatch restores session state before error handling, and response validation precedes session writes; see Session Changes and Failures.

Request-head limits run before tracing, routing, and session storage for both HTTP and WebSockets. Oversized requests use fresh local request IDs. The process-local security-failure limiter holds at most 10,000 client buckets, prunes expired state, and rejects new failure registrations at capacity without evicting active clients.

For application cookies, prefer Response.set_cookie() and Response.delete_cookie() so names, values, paths, and SameSite combinations are validated. Never pass a user-controlled next or return URL directly to redirect(); first require is_safe_redirect_target() to accept it as a relative same-origin target.

Validate user-controlled destinations before making a request:

from flasgo import Flasgo
app = Flasgo(
settings={
"SSRF_ALLOWED_SCHEMES": {"https"},
"SSRF_ALLOWED_HOSTS": {"api.example.com"},
}
)
async def resolve_target():
return await app.aresolve_outbound_url(
"https://api.example.com/v1/status"
)

Inside async handlers, use aresolve_outbound_url() so DNS uses the event loop resolver and is bounded by SSRF_RESOLUTION_TIMEOUT_SECONDS (5.0 seconds by default). Timeouts fail closed even when SSRF_ALLOW_UNRESOLVABLE_HOSTS=True. The synchronous resolve_outbound_url() remains available for synchronous code but blocks its calling thread during DNS resolution.

By default, Flasgo rejects unsafe schemes, embedded credentials, local and private network targets, unresolved hosts, and rebinding-prone unpinned destinations. Malformed URLs and invalid hostnames, such as an unterminated IPv6 literal or an empty or over-long label, raise SSRFViolation. Internationalized hostnames are converted to their IDNA ASCII form for allowlist matching, resolution, and target.host_header, and Unicode SSRF_ALLOWED_HOSTS entries match the same host. SSRF_ALLOW_PRIVATE_NETWORKS=True permits only RFC 1918 IPv4, unique-local IPv6, and deprecated site-local IPv6 (fec0::/10) destinations; loopback, link-local, multicast, reserved, and unspecified targets remain blocked. Use target.url, which contains the validated pinned address, as the connection target and target.host_header as the HTTP Host header when your HTTP client supports it. Do not fetch target.original_url, which performs another DNS lookup, and avoid logging its potentially sensitive query values. For HTTPS, the application-owned HTTP client must preserve the original hostname for TLS SNI and certificate verification while connecting to the pinned address. Flasgo provides URL validation/resolution helpers, not an outbound HTTP client. Revalidate every redirect; do not feed a redirect location directly to the client.

Only relax SSRF_ALLOW_PRIVATE_NETWORKS, SSRF_ALLOW_USERINFO, or SSRF_ALLOW_UNRESOLVABLE_HOSTS for a narrowly documented trust boundary.

Route policies, output contracts, and shared sessions

Section titled “Route policies, output contracts, and shared sessions”

Policy and Deployment Checks expose declared access rules and detect configuration drift. Blueprints add parent and child permissions without allowing public routes to weaken a protected group. Response Contracts filter declared public fields and fail with a safe 500 on invalid output.

Server-side sessions add opaque identifiers, rotation, revocation, and compare-and-swap updates. Store unavailability returns 503; a stale, expired, or revoked session write returns 409. Neither case should trigger blind replay of business side effects. Revocation affects future loads, not identities already acquired by active requests, streams, or WebSockets.

Streaming Responses bound output and connection lifetimes, close resources on disconnect, and terminate failed streams without appending diagnostic content. Complete critical transactions before headers using function-scoped dependencies.

Flasgo’s defaults reduce common risks involving access control, security misconfiguration, cryptographic misuse, injection into headers/templates, resource exhaustion, unsafe outbound requests, and missing security logging. That is defense coverage, not an automatic compliance claim. In application code:

  • validate input against explicit schemas and size limits;
  • enforce tenant and object ownership on every access;
  • parameterize database queries;
  • escape or sanitize output for its destination context;
  • pin, audit, and update dependencies;
  • never deserialize untrusted objects or execute untrusted code;
  • keep production errors generic and logs free of secrets;
  • test abuse cases alongside success cases.

See Configuration and settings for every security-related default.