Security
How AuthSaas handles tokens, passwords, and security events.
Token lifecycle#
Every authentication returns two tokens:
┌─────────────────────────────────────────────────┐
│ AuthSaas Token Flow │
├─────────────────────────────────────────────────┤
│ │
│ login() │
│ │ │
│ ├─ access_token (JWT, 15 min) ──► API │
│ └─ refresh_token (JWT, 7 days) │
│ │ │
│ └─ /auth/refresh ──► new pair │
│ │ │
│ └─ old refresh token REVOKED │
│ │
└─────────────────────────────────────────────────┘- Access token — signed JWT, expires in 15 minutes. Sent in every API request as
Authorization: Bearer <token>. - Refresh token — signed JWT, expires in 7 days. Used only to get a new token pair. Never sent to API endpoints.
Refresh token rotation#
AuthSaas uses single-use refresh token rotation. Every call to /auth/refresh invalidates the submitted token and issues a new pair. This means a stolen refresh token can only be used once — after which it's invalidated.
// The SDK handles this automatically.
// Manual rotation if needed:
const tokens = await authClient.refreshTokens();Reuse detection#
If a refresh token that has already been used is submitted again, AuthSaas detects the reuse and immediately revokes all refresh tokens for that user. This forces a full re-login and limits the blast radius of a token theft.
✕ Danger
TOKEN_REUSE, all user sessions are terminated. The user will need to log in again. This is by design — it indicates a potential token theft.Token storage#
The SDK stores tokens in sessionStorage by default:
- Access token — in memory (via React state), never written to persistent storage
- Refresh token — in
sessionStorage(cleared when tab closes)
⚠ Warning
localStorage — they persist across sessions and are accessible to any JavaScript on the page. If you need persistent sessions, use the httpOnly cookie mode (contact for configuration).Password hashing#
All passwords are hashed using bcrypt with a cost factor of 12 before storage. Plain-text passwords are never logged or stored at any point.
// Equivalent to what AuthSaas does server-side:
import bcrypt from 'bcryptjs';
const hash = await bcrypt.hash(password, 12); // ~400ms per hashAudit log#
Every auth event is written to an immutable audit log with:
- Event type (
register,login,logout,token_refresh) - User ID, app ID, tenant ID
- IP address
- Timestamp
Audit logs are queryable per app from the developer dashboard.
Best practices#
Restrict allowed origins#
When creating an app, set allowedOrigins to only the domains that will use theclientId. This prevents other sites from registering users under your app.
{
"allowedOrigins": [
"https://myapp.com",
"https://www.myapp.com"
]
}Protect your clientSecret#
The clientSecret is only needed for server-to-server calls. Never expose it in client-side code. Store it only in environment variables on your server.
Always use HTTPS#
Never send tokens over plain HTTP. All AuthSaas endpoints enforce HTTPS in production. Your app should do the same.
JWT algorithm pinning#
AuthSaas explicitly pins HS256 on both sign and verify — rejecting tokens with alg: none or any other algorithm.
Email verification#
Verification tokens are generated with crypto.randomBytes(32) — 256 bits of entropy. Only the SHA-256 hash of the token is stored in the database; the raw token is never persisted. This means a database breach cannot be used to craft valid verification links.
- Links expire in 24 hours
- Resend is rate-limited to 3 requests per 15 minutes per IP
Rate limiting#
AuthSaas enforces per-IP rate limits on sensitive auth endpoints:
- Login — 10 attempts per 15 minutes per IP. Exceeding the limit returns 429 with a
Retry-Afterheader. - Register — 5 attempts per hour per IP.
- Resend verification — 3 per 15 minutes per IP.
// 429 response body
{
"success": false,
"error": "Too many login attempts. Try again later.",
"code": "RATE_LIMITED"
}
// 429 response headers
Retry-After: 847ℹ Note
CORS enforcement#
All /auth/login and /auth/register requests that include an Origin header are validated against the app's allowedOrigins list. Requests without an Origin header (server-side SDK calls) are always allowed through without origin validation.
- Preflight
OPTIONSrequests are handled automatically. - CORS headers are only set when the origin is whitelisted — there is no wildcard
*fallback.
⚠ Warning
allowedOrigins to your exact production domains. localhost is fine for development.Password policy#
Passwords must satisfy all four rules:
- Minimum 8 characters, maximum 128 characters
- At least one uppercase letter (A–Z)
- At least one number (0–9)
- At least one special character (!@#$%…)
const passwordSchema = z.string()
.min(8).max(128)
.regex(/[A-Z]/, 'Needs uppercase')
.regex(/[0-9]/, 'Needs a number')
.regex(/[^A-Za-z0-9]/, 'Needs a special character');Security headers#
AuthSaas sets the following headers on every response:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()Content Security Policy#
AuthSaas sets a Content-Security-Policy header on all responses. The current policy is:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';frame-ancestors 'none'prevents the AuthSaas dashboard from being embedded in an<iframe>, protecting against clickjacking attacks. This is enforced in addition to theX-Frame-Optionsheader for maximum browser compatibility.- Future versions will adopt nonce-based CSP to eliminate
'unsafe-inline'and'unsafe-eval', providing stricter script control without breaking Next.js hydration.
Account lockout#
After 5 consecutive failed login attempts for the same email and app combination, the account is locked for 15 minutes. During this period all login attempts for that email+app return a 429 ACCOUNT_LOCKED response with a Retry-After header indicating the remaining lockout duration in seconds.
- The failed-attempt counter resets automatically on a successful login.
- Account lockout is separate from IP-based rate limiting — both checks apply simultaneously. A request can be blocked by either or both mechanisms.
// 429 response body
{
"success": false,
"error": "Account temporarily locked. Too many failed attempts.",
"code": "ACCOUNT_LOCKED"
}
// 429 response headers
Retry-After: 743Session management#
Every successful login creates a refresh token record (session) in the database, linked to the user, app, and device metadata. Sessions are the source of truth for active logins.
- Tenant admins can view all active sessions per app from Dashboard → Sessions.
- Individual sessions can be revoked at any time via the dashboard UI or the API.
- All sessions are automatically invalidated on password reset.
- Token reuse detection triggers family invalidation — every session for the affected user is immediately revoked.
GET /api/v1/sessions?appId=xxx — list active sessions (tenant auth)
DELETE /api/v1/sessions/:id — revoke a session (tenant auth)ℹ Note