Authentication & SSO
Auth methods (email/password, OAuth, magic link, OTP, TOTP/MFA), JWT RS256, refresh token families, enterprise SSO (OIDC/SAML), 47 provider presets, customer organizations, RLS, and setup wizard.
Overview
The AltBase auth system (atlas-auth crate) provides per-project user authentication with six auth methods plus MFA/TOTP, RS256 JWT tokens, refresh token rotation with family-based theft detection, enterprise SSO via OIDC and SAML 2.0, customer organizations for B2B multi-tenancy, row-level security (RLS) policy management, rate limiting per IP and email, and a setup wizard for guided configuration.
Each project gets its own RSA-2048 keypair. Private keys are encrypted at rest with AES-256-GCM using the platform ATLAS_MASTER_KEY. Public keys are exposed via a standard JWKS endpoint so any service can verify tokens without sharing secrets.
Key Concepts
Authentication Methods
| Method | Description | Endpoints |
|---|---|---|
| Email/Password | Argon2id hashing (19456 KiB memory, 2 iterations, 1 parallelism), configurable min password length | /auth/v1/signup, /auth/v1/token?grant_type=password |
| Magic Link | 32-byte token sent via email, 10-minute expiry, two-step flow to prevent link prefetcher consumption | /auth/v1/magiclink, /auth/v1/verify |
| Phone OTP | 6-digit numeric code via SMS, 5-minute expiry, max 3 verification attempts per code | /auth/v1/otp, /auth/v1/verify |
| OAuth 2.0 | Generic engine with PKCE (S256), Redis-backed state/verifier, configurable attribute mapping | /auth/v1/authorize, /auth/v1/callback |
| OIDC Discovery | Auto-configured from .well-known/openid-configuration, supports templated discovery URLs | Via SSO endpoints |
| SAML 2.0 | SP-initiated flow, XML signature verification via samael crate | /auth/v1/callback/saml |
JWT Structure
Access tokens are signed with RS256 (RSA-2048) and contain:
{
"sub": "<user_id>",
"aud": "authenticated",
"role": "authenticated",
"email": "user@example.com",
"email_verified": true,
"phone": "+1234567890",
"phone_verified": true,
"app_metadata": {},
"user_metadata": {},
"iss": "atlasdb",
"iat": 1710000000,
"exp": 1710003600
}
- Access token TTL: 3600 seconds (1 hour) by default, configurable via
jwt_access_ttl_seconds - Refresh token TTL: 604800 seconds (7 days) by default, configurable via
jwt_refresh_ttl_seconds - Anonymous requests:
role = "anon", nosubclaim
Refresh Token Families
Each refresh token belongs to a family_id. On rotation, a new token is issued with the same family and the old one is revoked. If a revoked token is reused (indicating a replay attack), the entire family is revoked, logging out all sessions for that user. This follows the OWASP recommendation for refresh token rotation.
Enterprise SSO (2-Layer Architecture)
AltBase SSO operates at two levels:
| Level | Scope | Use Case |
|---|---|---|
| Organization-level | Shared across all projects in an org | IT admin configures one Okta/Azure AD connection for the whole company |
| Project-level | Per-project activation and overrides | Individual apps opt-in to org SSO connections, with optional domain rules |
Organization SSO connections are stored in org_sso_connections with domain rules in org_sso_domain_rules. Projects activate specific connections via project_sso_activations and can add project-specific domain rules via project_sso_domain_rules.
Provider Presets
AltBase ships with 47 pre-configured provider presets loaded from a TOML registry. Each preset includes protocol, discovery URLs, default scopes, attribute mapping, and documentation links. You only need to supply your client_id and client_secret.
Big 4: Google, Apple, Microsoft, Facebook
Developer: GitHub, GitLab, Bitbucket
Messaging: Slack, Discord, Telegram, LINE
Social: Twitter/X, LinkedIn, Twitch, Spotify, TikTok, Reddit
Productivity: Notion, Zoom, Figma, Dropbox, Shopify
Enterprise IdPs: Okta, Azure AD / Entra ID, Auth0, OneLogin, PingIdentity, JumpCloud, Google Workspace, Keycloak, Salesforce
Customer Organizations (B2B)
Customer organizations enable B2B multi-tenancy within a single project. End users belong to customer orgs, and RLS policies can reference the organization context for data isolation between tenants.
Row-Level Security (RLS)
AltBase manages PostgreSQL RLS through a hybrid model:
Declarative policies — JSON definition translated to SQL:
| Pattern | Definition JSON | Generated SQL |
|---|---|---|
| Owner-based | {"column": "user_id", "match": "auth_uid()"} | USING (user_id = auth_uid()) |
| Role-based | {"role": "admin"} | USING (auth_role() = 'admin') |
| Public read | {"public": true} | USING (true) for SELECT only |
| Combined | {"or": [{"column": "user_id", "match": "auth_uid()"}, {"role": "admin"}]} | USING (user_id = auth_uid() OR auth_role() = 'admin') |
Raw SQL policies — {"sql": "USING (...)"}, validated to reject DDL keywords and semicolons, executed within a non-superuser role scoped to the tenant schema.
Helper functions are created in each tenant schema during provisioning:
-- Returns the authenticated user's UUID
SELECT auth_uid();
-- Returns the user's role ('authenticated' or 'anon')
SELECT auth_role();
-- Returns the user's email
SELECT auth_email();
These functions read JWT claims injected via SET LOCAL request.jwt.claim.* before every query.
API Reference
Core Auth Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /auth/v1/signup | Register with email/password | API Key |
POST | /auth/v1/token?grant_type=password | Sign in with email/password | API Key |
POST | /auth/v1/token?grant_type=refresh_token | Refresh access token | API Key |
POST | /auth/v1/verify | Verify email/recovery/OTP/magic link token | API Key |
POST | /auth/v1/recover | Send password recovery email | API Key |
POST | /auth/v1/logout | Revoke session (optional {"scope": "global"} to revoke all) | API Key + JWT |
GET | /auth/v1/user | Get current user profile | API Key + JWT |
PUT | /auth/v1/user | Update user metadata, email, or password | API Key + JWT |
GET | /auth/v1/.well-known/jwks.json | JWKS public key endpoint (cached 1 hour in Redis) | None |
OAuth Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /auth/v1/authorize | Start OAuth/OIDC flow (redirects to provider) | API Key |
GET | /auth/v1/callback | OAuth/OIDC callback (exchanges code for tokens) | None |
POST | /auth/v1/callback/saml | SAML 2.0 assertion consumer endpoint | None |
Magic Link and OTP
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /auth/v1/magiclink | Send magic link email | API Key |
POST | /auth/v1/otp | Send OTP via SMS or email | API Key |
SSO Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /auth/v1/providers | List available SSO providers for the project | API Key |
GET | /auth/v1/sso/authorize/:connection_id | Start SSO flow | API Key |
GET | /auth/v1/sso/callback | OIDC SSO callback | None |
POST | /auth/v1/sso/link | Link SSO identity to current user | API Key + JWT |
DELETE | /auth/v1/sso/link/:identity_id | Unlink SSO identity | API Key + JWT |
GET | /auth/v1/sso/identities | List linked SSO identities | API Key + JWT |
Auth Management (Service Key)
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/projects/{id}/auth/settings | Get auth settings | Service Key |
PUT | /v1/projects/{id}/auth/settings | Update auth settings | Service Key |
POST | /v1/projects/{id}/auth/settings | Initialize auth (generates RSA keypair) | Service Key |
POST | /v1/projects/{id}/auth/rotate-keys | Rotate RSA keypair (grace period = access TTL) | Service Key |
RLS Management (Service Key)
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/projects/{id}/rls/enable | Enable RLS on a table | Service Key |
POST | /v1/projects/{id}/rls/disable | Disable RLS on a table | Service Key |
GET | /v1/projects/{id}/rls/policies | List RLS policies | Service Key |
POST | /v1/projects/{id}/rls/policies | Create an RLS policy | Service Key |
DELETE | /v1/projects/{id}/rls/policies/{policy_id} | Delete an RLS policy | Service Key |
Setup Wizard (Service Key)
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/projects/{id}/setup/recommend | Get recommended auth configuration | Service Key |
POST | /v1/projects/{id}/setup/apply | Apply recommended configuration | Service Key |
POST | /v1/projects/{id}/setup/validate-draft | Validate a draft configuration | Service Key |
GET | /v1/projects/{id}/setup/status | Get setup completion status | Service Key |
Code Examples
Email/Password Authentication
# Sign up a new user
curl -X POST http://localhost:3000/auth/v1/signup \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"user_metadata": {"display_name": "Alice"}
}'
# Sign in
curl -X POST "http://localhost:3000/auth/v1/token?grant_type=password" \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "securepassword123"}'
# Response:
# {
# "access_token": "eyJ...",
# "refresh_token": "rt_abc123...",
# "token_type": "bearer",
# "expires_in": 3600,
# "user": { "id": "uuid", "email": "user@example.com", ... }
# }
# Refresh access token
curl -X POST "http://localhost:3000/auth/v1/token?grant_type=refresh_token" \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "rt_abc123..."}'
OAuth Flow
# Start OAuth flow (redirects to provider)
curl -G "http://localhost:3000/auth/v1/authorize" \
--data-urlencode "provider=google" \
--data-urlencode "redirect_to=https://myapp.com/auth/callback" \
-H "Authorization: Bearer $ANON_KEY"
# Returns 302 redirect to Google with PKCE challenge and state parameter
Magic Link
# Send magic link
curl -X POST http://localhost:3000/auth/v1/magiclink \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# User clicks link, app extracts token, then verifies:
curl -X POST http://localhost:3000/auth/v1/verify \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "magic_link", "token": "abc123..."}'
Phone OTP
# Send OTP
curl -X POST http://localhost:3000/auth/v1/otp \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+1234567890"}'
# Verify OTP
curl -X POST http://localhost:3000/auth/v1/verify \
-H "Authorization: Bearer $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "phone_otp", "phone": "+1234567890", "code": "123456"}'
RLS Policy Management
# Enable RLS on a table
curl -X POST http://localhost:3000/v1/projects/$PROJECT_ID/rls/enable \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"table": "todos"}'
# Create a declarative owner-based read policy
curl -X POST http://localhost:3000/v1/projects/$PROJECT_ID/rls/policies \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"table_name":"todos","policy_name":"owner_read","operation":"SELECT","policy_type":"declarative","definition":{"column":"user_id","match":"auth_uid()"}}'
# Create a raw SQL policy for admin access
curl -X POST http://localhost:3000/v1/projects/$PROJECT_ID/rls/policies \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"table_name":"todos","policy_name":"admin_all","operation":"SELECT","policy_type":"raw_sql","definition":{"sql":"auth_role() = '\''admin'\''"}}'
SDK Usage
import { createClient } from '@altbasedb/sdk'
const client = createClient('http://localhost:3000', 'YOUR_ANON_KEY')
// Sign up
const { data, error } = await client.auth.signUp({
email: 'user@example.com',
password: 'securepassword123',
options: { data: { display_name: 'Alice' } }
})
// Sign in
const { data: session } = await client.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword123'
})
// OAuth
const { data: oauthData } = await client.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'https://myapp.com/auth/callback' }
})
// Get user
const { data: user } = await client.auth.getUser()
// Sign out
await client.auth.signOut()
Configuration
Environment Variables
| Variable | Description |
|---|---|
ATLAS_MASTER_KEY | 32-byte hex key for AES-256-GCM encryption of RSA private keys and tenant secrets |
ATLAS_PLATFORM_SMTP_HOST | SMTP server for verification, magic link, and recovery emails |
ATLAS_PLATFORM_SMTP_PORT | SMTP port (default: 587) |
ATLAS_PLATFORM_SMTP_USER | SMTP username |
ATLAS_PLATFORM_SMTP_PASS | SMTP password |
ATLAS_PLATFORM_SMTP_FROM | Sender email address (default: noreply@atlasdb.io) |
ATLAS_PLATFORM_SMS_ENDPOINT | SMS API endpoint for OTP delivery |
ATLAS_PLATFORM_SMS_API_KEY | SMS API key |
Per-Project Auth Settings
Stored in project_auth_settings in the control plane database:
| Setting | Default | Description |
|---|---|---|
jwt_access_ttl_seconds | 3600 | Access token lifetime |
jwt_refresh_ttl_seconds | 604800 | Refresh token lifetime (7 days) |
enable_signup | true | Allow new user registration |
enable_email_verify | true | Send verification emails on signup |
enforce_email_verification | false | Block login until email is verified |
enable_magic_link | false | Enable magic link authentication |
enable_phone_otp | false | Enable phone OTP authentication |
enable_cookie_auth | false | Enable cookie-based auth (for SSR frameworks) |
min_password_length | 8 | Minimum password length |
Cookie-Based Auth
When enable_cookie_auth is true, sign-in and token refresh set HttpOnly cookies:
| Cookie | Attributes | Max-Age |
|---|---|---|
sb-access-token | HttpOnly, Secure, SameSite=Lax, Path=/ | 3600 |
sb-refresh-token | HttpOnly, Secure, SameSite=Lax, Path=/auth/v1/token | 604800 |
Token resolution order: Authorization: Bearer header takes precedence over sb-access-token cookie. CSRF protection requires x-csrf-token header on all mutation endpoints when cookie auth is active. CSRF token is returned in the JSON response body of sign-in endpoints.
Rate Limiting on Auth Endpoints
| Action | Limit | Window |
|---|---|---|
| Email sending (verification, magic link, recovery) | 5 per email address | 1 hour |
| SMS sending | 5 per phone number | 1 hour |
| OTP verification attempts | 3 per code | Code lifetime |
| Failed sign-in | 10 per IP | 15 minutes |
| Sign-up | 10 per IP | 1 hour |
How It Works
Signup Flow
- Client sends
POST /auth/v1/signupwith email, password, and optional user_metadata - Rate limiter checks per-IP and per-email limits in Redis
- Password validated against
min_password_lengthsetting - Password hashed with Argon2id (memory 19456 KiB, iterations 2, parallelism 1)
- User row inserted into
{schema}.userswithprovider = 'email' - If
enable_email_verifyis on: 32-byte token generated, SHA-256 hash stored inverification_tokens, verification email sent - RSA private key decrypted from
project_auth_settingsusingATLAS_MASTER_KEY - JWT access + refresh tokens signed (RS256) and returned
- Auth event published to NATS for the trigger system
OAuth Flow (PKCE)
GET /auth/v1/authorize?provider=google&redirect_to=...- Load provider config from
oauth_providerstable (or SSO connection) - Generate random
stateparameter, store in Redis (5-min TTL) withredirect_to - Generate PKCE
code_verifier+code_challenge(S256), store verifier in Redis keyed by state - Redirect to provider's
authorize_urlwithclient_id,redirect_uri,scope,state,code_challenge - Provider authenticates user and redirects to
GET /auth/v1/callback?code=...&state=... - Validate
stateagainst Redis (CSRF check) - Retrieve PKCE
code_verifierfrom Redis - Exchange
codefor tokens at provider'stoken_url(withcode_verifier) - Fetch user info from provider's
userinfo_url - Apply
attribute_mappingto extract email, name, avatar - Upsert user: match by
provider+provider_id, fall back to email match - Issue JWT pair, redirect to
redirect_toURL with tokens
Refresh Token Rotation
- Client sends refresh token to
/auth/v1/token?grant_type=refresh_token - Token is SHA-256 hashed and looked up in
refresh_tokenstable - Verify
revoked = falseandexpires_at > now() - Issue new refresh token with same
family_id, revoke old token - Theft detection: if a revoked token is reused, revoke ALL tokens in that
family_id - Return new JWT access + refresh tokens
RLS Policy Evaluation
- RLS enabled on a table via
ALTER TABLE ... ENABLE ROW LEVEL SECURITY; ALTER TABLE ... FORCE ROW LEVEL SECURITY; - Policies define SQL conditions using
auth_uid(),auth_role(),auth_email() - The API engine sets
request.jwt.claim.*as PostgreSQL GUC variables viaSET LOCALbefore every query - PostgreSQL evaluates the RLS policy on every row access
- Realtime CDC fan-out also evaluates RLS predicates before delivering events
Key Rotation
POST /v1/projects/{id}/auth/rotate-keys generates a new RSA-2048 keypair. The JWKS endpoint serves both old and new keys (identified by kid in the JWT header) for a grace period equal to jwt_access_ttl_seconds after rotation, then drops the old key. Existing JWTs remain verifiable during the transition.
Security Details
- Token storage: All tokens (refresh, magic link, OTP) are stored as SHA-256 hashes in the database, never plaintext
- Secret encryption: All tenant secrets (
client_secret_encrypted,smtp_pass_encrypted,sms_api_key_encrypted) encrypted with AES-256-GCM usingATLAS_MASTER_KEY, decrypted only in-memory at use time, never logged - OAuth state: Stored in Redis with 5-minute TTL, not in the database
- PKCE: S256 code challenge method used for all OAuth flows
- Raw SQL policy mitigation: Raw SQL policies are wrapped in
CREATE POLICY ... USING (...), validated to reject DDL keywords and semicolons, executed within a non-superuser role scoped to the tenant schema
Error Codes
| Code | HTTP | Description |
|---|---|---|
invalid_grant | 401 | Bad credentials, expired or revoked token |
user_banned | 403 | Account banned (check banned_until field) |
email_not_verified | 403 | Login blocked when enforce_email_verification is true |
signup_disabled | 403 | Project has enable_signup = false |
rate_limited | 429 | Too many attempts (check Retry-After header) |
provider_error | 502 | OAuth/OIDC/SAML provider error |
invalid_token | 401 | Malformed or expired JWT |
csrf_mismatch | 403 | Cookie mode, missing or invalid CSRF token |
policy_error | 400 | Invalid RLS policy definition |
transport_error | 502 | Email or SMS delivery failure |
Database API Engine
PostgREST-compatible REST CRUD, query operators, embedding/joins, pagination, raw SQL endpoint, schema introspection, two-tier caching, query compiler, and index advisor.
Storage
Buckets, object CRUD, multipart uploads, TUS v1.0.0 protocol, storage providers (filesystem, S3, Azure Blob), signed URLs, image transforms, storage policies, and background cleanup.