# Security and Authentication

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

Source: https://flasgo.dev/guides/security-and-auth/

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

## Production baseline

```python
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`.

## Authentication and permissions

Register a backend, then attach permissions to routes:

```python
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.

### Strict JWT authentication

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

```bash
uv add flasgo --extra jwt
```

```python
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.

### OpenAPI security metadata

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](/reference/api-documentation/) for OAuth2, OpenID Connect, and custom scheme examples.

## Route rate limits

Add an in-process sliding-window limit:

```python
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:

```python
@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](/guides/shared-storage/). Security-failure and per-WebSocket message throttling remain process-local, so retain aggregate protections at a trusted edge.

## CSRF, hosts, sessions, and headers

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](/reference/configuration-and-settings/#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](/guides/http-apps/#cross-origin-browser-apis) 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](/guides/http-apps/#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.

## Outbound URL and SSRF protection

Validate user-controlled destinations before making a request:

```python
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

[Policy and Deployment Checks](/guides/policy-and-deployment-checks/) expose declared access rules and detect configuration drift. [Blueprints](/guides/blueprints-and-url-generation/) add parent and child permissions without allowing public routes to weaken a protected group. [Response Contracts](/guides/response-contracts/) filter declared public fields and fail with a safe `500` on invalid output.

[Server-side sessions](/guides/shared-storage/) 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](/guides/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.

## OWASP Top 10 2025 posture

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](/reference/configuration-and-settings/) for every security-related default.
