# Sessions and security

> Session lifetime, single sign-on, sign-out, brute-force lockouts, bot protection and SMS fraud limits.

Source: https://9in.io/docs/security/

## Sessions and single sign-on

When a user signs in, 9in starts a session on the tenant's domain (an http-only cookie). Every application of the tenant shares it: a user signed in for one app isn't asked again by the next (single sign-on), unless that app sends `prompt=login` or `max_age`.

- **Lifetime:** 30 days by default (**Security → Sessions**), extended as the user comes back (sliding expiry).
- **Refresh tokens** belong to a session: they stop working when it ends.
- **Users** see their sessions on the account page and can end any of them, or **Sign out everywhere**.
- **Admins** can end sessions in the console (Users → user, or Sessions) or with the [Management API](https://9in.io/docs/reference/management-api/).

## Signing out

| What | How | Effect |
|---|---|---|
| Your app only | Delete your app's tokens and cookie | The 9in session stays: the next sign-in is instant |
| Your app and 9in | Redirect to `<issuer>/oauth/logout?id_token_hint=…&post_logout_redirect_uri=…` (`auth.logout()`) | Ends the 9in session; other apps' refresh tokens from it stop working |
| Everywhere | Account page → Sign out everywhere, or `DELETE /api/v1/users/{id}/sessions` | Ends every session of the user |

`post_logout_redirect_uri` must be one of the application's **Allowed logout URLs**. Without one, the user sees a "You're signed out" page. Unknown or missing URLs never redirect elsewhere.

## Revocation

Signing out, ending a session, blocking or deleting a user, refresh-token reuse and `POST /oauth/revoke` all take effect on 9in at once: refresh tokens stop working, introspection reports the tokens inactive, and `token.revoked` / `session.revoked` [webhooks](https://9in.io/docs/webhooks/) go out. JWT access tokens verified **locally** keep working until they expire; use hybrid verification to see revocations within seconds ([Protecting your API](https://9in.io/docs/protecting-apis/)).

**Refresh-token rotation:** every refresh returns a new refresh token and invalidates the old one. If an old one is ever used again, which only happens when a token was stolen or copied, 9in revokes the whole family of tokens from that sign-in and records `token.reuse_detected` in the activity log.

## Brute-force protection

- **Accounts:** after 10 failed sign-ins (3 to 100), the account is locked for 15 minutes (1 minute to 24 hours). The counter resets after a successful sign-in.
- **IP addresses:** locked after ten times as many failures, across accounts.
- **One-time codes:** 5 attempts per code (1 to 10), and limits on how many codes one address or IP can request.
- Locked users see "Too many attempts. Please try again later."

## Bot protection

**Security → Bot protection** adds Cloudflare Turnstile (invisible for most people, no puzzles) to:

- sign-up and password recovery (on by default once enabled);
- sign-in: never, only after failures (default), or always.

9in's own Turnstile widget covers the `*.9in.io` hostnames. [Custom domains](https://9in.io/docs/custom-domains/) need your own widget (site key and secret key), because Turnstile checks the hostname.

## SMS fraud

Text messages cost money, which makes sign-up and code pages a target for "SMS pumping". 9in limits it:

- **Allowed countries** (Security → SMS delivery): texts only go to numbers whose calling code you picked. Countries that share a code (+1 for the US, Canada and the Caribbean) are allowed together.
- **Daily cap** on texts sent through 9in's account per tenant (per plan), and per-number and per-IP limits on code requests.
- Refusals are logged (`sms.blocked`) and shown as console alerts.

## The hosted pages

- Served only over HTTPS, with `X-Frame-Options: DENY` and `frame-ancestors 'none'` (no clickjacking), `nosniff`, `no-referrer`, `no-store` and a strict Content Security Policy: scripts need the page's nonce, and custom themes declare the outside origins they use.
- Every form has CSRF protection; sign-in transactions expire.
- Passwords are hashed with argon2id; secrets you store (client secrets, Twilio tokens, signing keys) are encrypted at rest.
