
# Web security of hosted pages and the front API

Hosted login, the account portal, the email pages (verification, password reset, invitation) and the browser API they call follow a fixed web security baseline on every plan. These protections are not settings you can switch off. The only setting you control is which web origins may call the OAuth endpoints from the browser.

## Cross-site request forgery (CSRF)

Requests that change state are checked before anything is read or changed:

- **Origin check.** A state-changing request to the hosted sign-in API or to a verification or reset page must carry an `Origin` header that is exactly the environment's login host. Any other origin is refused with `origin_not_allowed`.
- **Consent screen.** A form post on the consent screen is refused when its `Origin` is not the page's own host, or when `Sec-Fetch-Site` is present and is not `same-origin`.
- **Form tokens.** Each sign-in transaction and each email form has its own CSRF token, derived with HMAC-SHA256 from a per-environment key. The sign-in API expects it in the `Idp-Csrf-Token` header; the token of one form does not work for another. Tokens are compared in constant time.
- **Cookies.** The cookies that carry sign-in state use `SameSite=Lax` (see below), so a request started from another site does not send them.

## Cross-origin requests (CORS)

CORS is answered only on the OAuth endpoints a browser application calls: the token, revocation, pushed authorization request and userinfo endpoints. It applies to public single-page applications (`spa` type) only.

- Set the allowed origins per application with `allowed_web_origins` (`https://host[:port]`, at most 20, no wildcards). See [Create an application](/docs/api/management/applications/create).
- A browser's preflight request does not say which application is calling, so it is answered from the union of the allowed origins of the environment's active public single-page applications. The actual request is then checked against the origin list of the application that is calling.
- An origin outside that application's list is refused with `403 origin_not_allowed` and receives no `Access-Control-Allow-Origin` header.
- An allowed origin is echoed back exactly. `*` and `null` are never sent, `Access-Control-Allow-Credentials` is never sent, and every cross-origin answer carries `Vary: Origin`.
- Requests from the login host itself are not cross-origin and are not affected.

## Redirects after sign-in

If you use the Next.js SDK, `returnTo` must be a path of your own app. A full URL or a value such as `//evil.example` is ignored and the user lands on `/`. See [Next.js SDK](/docs/sdk/nextjs).

## Security headers

Hosted pages set these headers on every response:

| Header | Value |
|---|---|
| `Content-Security-Policy` | `default-src 'none'`; scripts only with a fresh per-response nonce; `style-src 'self'`; `connect-src 'self'`; `frame-ancestors 'none'`; `base-uri 'none'`; `object-src 'none'`; `upgrade-insecure-requests`; Trusted Types required for scripts |
| `Reporting-Endpoints` and `report-to` | Policy violations are reported to `/u/csp-report` on the same host |
| `X-Frame-Options` | `DENY`: hosted pages cannot be embedded in a frame |
| `X-Content-Type-Options` | `nosniff` |
| `Referrer-Policy` | `no-referrer` |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Permissions-Policy` | Camera, microphone, geolocation, payment, USB, serial, HID and Bluetooth are off; passkey requests are allowed for the page itself |
| `Cache-Control` | `no-store` |

The policy allows no inline scripts and no `unsafe-` keywords. Form submissions are limited to the page's own host; on the consent screen the policy also allows the redirect address of the application that is asking, so the decision can return to it.

The report endpoint accepts only browser violation reports of at most 8 KiB, and is rate limited per IP address. Reports are counted by directive. They carry no query string, fragment or user information when logged.

### Strict-Transport-Security

- Environment hosts on `keenauth.app` send `max-age=63072000; includeSubDomains`.
- A [custom domain](/docs/guides/custom-domains) sends `max-age=63072000` only, without `includeSubDomains`. KeenAuth cannot decide for the other subdomains of your domain, so that choice stays with you.

## Cookies

Sign-in state cookies are named with the `__Host-` prefix, so a browser accepts them only over HTTPS, with `Path=/` and without a `Domain` attribute. They cannot be set from a sibling subdomain.

| Cookie | Flags | Purpose |
|---|---|---|
| `__Host-idp_session` | `Secure`, `HttpOnly`, `SameSite=Lax` | The user's session. Its lifetime ends at the session's absolute expiry |
| `__Host-idp_device` | `Secure`, `HttpOnly`, `SameSite=Lax` | Device recognition |
| `__Host-idp_ltx` and `__Host-idp_azr_*` | `Secure`, `HttpOnly`, `SameSite=Lax` | Binds a sign-in or authorization attempt to the browser that started it |
| `__Host-idp_fed_bind` | `Secure`, `HttpOnly`, `SameSite=None` | Short-lived binding for enterprise SAML sign-in. The identity provider posts back from another site, so `Lax` would drop it |

Cookie values are not readable by page scripts (`HttpOnly`).

## Related

- [Dashboard access security](/docs/security/dashboard-access)
- [Tenant isolation](/docs/security/tenant-isolation)
- [Brand your login pages](/docs/guides/branding)
