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

On 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

Never store tokens in 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 hash

Audit 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-After header.
  • 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

The current implementation is per-instance (in-process memory). For distributed or serverless deployments, upgrade to Upstash Redis for accurate cross-instance rate limiting.

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 OPTIONS requests are handled automatically.
  • CORS headers are only set when the origin is whitelisted — there is no wildcard * fallback.

Warning

Always set 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 the X-Frame-Options header 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: 743

Session 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

Both endpoints require tenant-level authentication (your dashboard JWT). End users cannot enumerate or revoke other users' sessions.