Sessions and security
Session lifetime, single sign-on, sign-out, brute-force lockouts, bot protection and SMS fraud limits.
#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.
#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 go out. JWT access tokens verified locally keep working until they expire; use hybrid verification to see revocations within seconds (Protecting your API).
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 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: DENYandframe-ancestors 'none'(no clickjacking),nosniff,no-referrer,no-storeand 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.