Documentation Index
Fetch the complete documentation index at: https://docs.snackbase.dev/llms.txt
Use this file to discover all available pages before exploring further.
SnackBase provides a comprehensive authentication system designed for multi-tenant applications. This guide explains authentication flows, token management, API keys, multi-account users, email verification, OAuth/SAML integration, and security considerations.
Overview
SnackBase authentication is built for enterprise multi-account scenarios:
| Feature | Description |
|---|
| Account-Scoped Users | Users belong to specific accounts |
| Multi-Account Support | Same email can exist in multiple accounts |
| Per-Account Passwords | Different passwords per (email, account) tuple |
| JWT Tokens | Access tokens (1 hour) + Refresh tokens (7 days) |
| Token Rotation | Refresh token rotation on each use with revocation |
| API Keys | Service authentication with hashed keys |
| Email Verification | Required for login, tokens expire in 1 hour |
| Multi-Provider | Support for Password, OAuth, and SAML providers |
| Identity Linking | Link local accounts with external provider identities |
| Timing-Safe Comparison | Password verification resistant to timing attacks |
| Hierarchical Config | System-level and account-level provider settings |
User Identity Model
In SnackBase, a user’s identity is defined by a tuple:
(email, account_id) = unique user identity
This means:
Account Registration
Account registration creates a new tenant/workspace in SnackBase.
Accounts use two identifiers:
# Example account
{
"id": "550e8400-e29b-41d4-a716-446655440000", # UUID (primary key)
"account_code": "AB1001", # Human-readable code
"slug": "acme-corp", # URL-friendly identifier
"name": "Acme Corp" # Display name
}
Properties:
- id (UUID): Primary key, immutable, globally unique
- account_code (XX####): Human-readable format for display
- Format: 2 letters + 4 digits (e.g., AB1001, XY2048)
- Sequential generation for easy reference
- Used in UI and exports
- slug: URL-friendly identifier for login
- name: Display name (not unique)
User Registration
User registration creates a new user within a specific account.
Registration Flow
┌──────────────┐
│ User fills │
│ registration │
│ form │
└──────┬───────┘
│
▼
┌─────────────────────────────────┐
│ POST /api/v1/auth/register │
│ { │
│ "account": "acme-corp", │
│ "email": "[email protected]", │
│ "password": "SecurePass123!", │
│ "name": "Alice Johnson" │
│ } │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Resolve account by slug │
│ "acme-corp" → account_id │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 2. Validate email uniqueness │
│ (within account) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3. Validate password strength │
│ - Min 8 chars │
│ - Uppercase, lowercase │
│ - Number, special char │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4. Hash password (Argon2id) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 5. Create user record │
│ - id: user_abc123 │
│ - account_id: <uuid> │
│ - email: [email protected] │
│ - password_hash: <argon2 hash> │
│ - email_verified: false │
│ - auth_provider: "password" │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 6. Generate verification token │
│ - SHA-256 hash │
│ - 1 hour expiration │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 7. Send verification email │
│ To: [email protected] │
└─────────────────────────────────┘
Email Uniqueness
Email uniqueness is scoped to account:
Email Verification
Email verification is required before users can log in to their accounts.
Verification Flow
┌─────────────────────────────────┐
│ User completes registration │
│ Account created │
│ email_verified: false │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ System generates verification │
│ token (random 32-byte string) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Token hashed with SHA-256 │
│ Stored in email_verifications │
│ - token_hash: <sha256> │
│ - expires_at: now() + 1 hour │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Verification email sent │
│ Subject: Verify your email │
│ Contains verification link │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ User clicks link │
│ GET /auth/verify-email?token=...│
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Hash provided token │
│ SHA-256(token) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 2. Lookup token_hash in DB │
│ Check not expired │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3. Update user record │
│ - email_verified: true │
│ - email_verified_at: now() │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4. Delete verification token │
│ (single-use only) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 5. Return success response │
│ User can now login │
└─────────────────────────────────┘
Verification Token Model
# Email Verification Token
{
"id": "ev_abc123",
"user_id": "user_xyz789",
"token_hash": "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e", # SHA-256
"expires_at": "2025-01-01T01:00:00Z", # 1 hour from creation
"created_at": "2025-01-01T00:00:00Z"
}
Security Properties:
- Tokens are hashed with SHA-256 before storage (never stored in plaintext)
- Tokens expire after 1 hour
- Tokens are single-use (deleted after verification)
- Token hash uses constant-time comparison to prevent timing attacks
Login Requirement
Users cannot login until their email is verified:
# Login check
if not user.email_verified:
raise HTTPException(
status_code=401,
detail="Email not verified. Please check your inbox."
)
Login Flow
Login authenticates a user and issues JWT tokens.
Login Process
┌──────────────┐
│ User enters │
│ credentials: │
│ - account │
│ - email │
│ - password │
└──────┬───────┘
│
▼
┌─────────────────────────────────┐
│ POST /api/v1/auth/login │
│ { │
│ "account": "acme-corp", │
│ "email": "[email protected]", │
│ "password": "SecurePass123!" │
│ } │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Resolve account by slug │
│ "acme-corp" → account_id │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 2. Find user by (email, account)│
│ WHERE email = ? │
│ AND account_id = ? │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3. Check email verification │
│ if not verified: 401 Error │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4. Timing-safe password verify │
│ argon2.verify(password_hash, │
│ provided_password)│
│ (uses dummy hash if no user) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 5. Generate tokens │
│ - Access token (1 hour) │
│ - Refresh token (7 days) │
│ - Store refresh token hash │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 6. Return tokens │
│ { │
│ "access_token": "...", │
│ "refresh_token": "...", │
│ "token_type": "bearer", │
│ "user": { ... } │
│ } │
└─────────────────────────────────┘
Timing-Safe Password Comparison
SnackBase uses timing-safe comparison to prevent timing attacks:
# ❌ VULNERABLE: Regular comparison (timing leak)
if user.password_hash == provided_password:
# Attacker can measure time to guess password
# ✅ SECURE: Timing-safe comparison
# Also uses dummy hash for non-existent users
if argon2.verify(user.password_hash, provided_password):
# Constant time regardless of match
Token Management
SnackBase uses JWT (JSON Web Tokens) with access and refresh tokens, with true token rotation for enhanced security.
Token Types
| Token Type | Lifetime | Purpose | Storage | Database |
|---|
| Access Token | 1 hour | API requests | localStorage/memory | No |
| Refresh Token | 7 days | Get new access token | HttpOnly cookie or localStorage | Yes |
Access Token Structure
{
"sub": "user_abc123", // Subject (user ID)
"account_id": "550e8400-...", // Account context (UUID)
"email": "[email protected]", // User email
"role": "admin", // User role
"exp": 1704067200, // Expiration timestamp
"iat": 1704063600 // Issued at timestamp
}
Refresh Token Structure
{
"sub": "user_abc123", // Subject (user ID)
"account_id": "550e8400-...", // Account context (UUID)
"jti": "token_xyz789", // JWT ID (unique token identifier)
"exp": 1704668400, // Expiration timestamp (7 days)
"iat": 1704063600 // Issued at timestamp
}
The jti (JWT ID) claim uniquely identifies each refresh token and is used to track revocation.
Token Refresh with Rotation
# Refresh access token
POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // NEW!
"token_type": "bearer"
}
True Token Rotation:
- Old refresh token is marked as revoked in database
- New refresh token is generated and stored (hash)
- Old token cannot be used again (returns 401 if attempted)
- Each refresh creates a new token in the chain
OAuth 2.0 Authentication
SnackBase supports OAuth 2.0 / OpenID Connect authentication for popular social and enterprise identity providers.
Supported OAuth Providers
| Provider | Description |
|---|
| Google | Google Account login |
| GitHub | GitHub account login |
| Microsoft | Microsoft / Azure AD login |
| Apple | Sign in with Apple |
OAuth Flow
┌──────────────┐
│ User clicks │
│ "Login with │
│ Google" │
└──────┬───────┘
│
▼
┌─────────────────────────────────┐
│ GET /oauth/google/authorize │
│ ?account=acme-corp │
│ &client_state=abc123 │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Generate state token │
│ 2. Encode RelayState │
│ 3. Redirect to Google │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ User authenticates │
│ with Google │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Google redirects back │
│ GET /oauth/google/callback? │
│ code=...& │
│ state=...& │
│ relay_state=... │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Verify state token │
│ 2. Decode RelayState │
│ 3. Exchange code for tokens │
│ 4. Get user info │
│ 5. Find or create user │
│ 6. Update user record │
│ 7. Generate JWT tokens │
│ 8. Redirect to client app │
└─────────────────────────────────┘
SAML 2.0 Authentication
SnackBase supports SAML 2.0 for enterprise single sign-on (SSO) with identity providers like Okta, Azure AD, and other SAML-compliant systems.
Supported SAML Providers
| Provider | Description |
|---|
| Okta | Okta Identity Cloud SSO |
| Azure AD | Microsoft Azure Active Directory |
| Generic | Any SAML 2.0 compliant IdP |
SAML Flow
┌──────────────┐
│ User clicks │
│ "Login with │
│ SSO" │
└──────┬───────┘
│
▼
┌─────────────────────────────────┐
│ GET /saml/{provider}/sso │
│ ?account=acme-corp │
│ &client_state=abc123 │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Resolve SAML config │
│ 2. Generate SAML request │
│ 3. Encode RelayState │
│ 4. Redirect to IdP │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ User authenticates │
│ with IdP (e.g., Okta) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ IdP posts SAML response │
│ POST /saml/{provider}/acs │
│ - SAMLResponse=<base64> │
│ - RelayState=<base64> │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 1. Decode RelayState │
│ 2. Verify SAML response │
│ 3. Extract user attributes │
│ 4. Find or create user │
│ 5. Update user record │
│ 6. Generate JWT tokens │
│ 7. Redirect to client app │
└─────────────────────────────────┘
Multi-Account Users
SnackBase supports enterprise multi-account scenarios where users can belong to multiple accounts.
User Identity Matrix
┌────────────────────┬──────────────┬──────────────┬──────────────┐
│ email │ account_id │ password │ role │
├────────────────────┼──────────────┼──────────────┼──────────────┤
│ [email protected] │ 550e8400-... │ Password1! │ admin │
│ [email protected] │ 660e8400-... │ Password2! │ viewer │
│ [email protected] │ 550e8400-... │ Password3! │ editor │
│ [email protected] │ 660e8400-... │ Password4! │ admin │
└────────────────────┴──────────────┴──────────────┴──────────────┘
Key Points:
- Same email can exist in multiple accounts
- Each
(email, account_id) tuple has a unique password
- Users must specify account when logging in
Login with Account Selection
When logging in, users must specify which account they’re accessing:
Option 1: Account in URL (subdomain)
POST https://acme-corp.snackbase.dev/api/v1/auth/login
{
"email": "[email protected]",
"password": "Password1!"
}
Option 2: Account in Request Body
POST https://snackbase.dev/api/v1/auth/login
{
"account": "acme-corp", // Account slug
"email": "[email protected]",
"password": "Password1!"
}
API Key Authentication
API keys provide an alternative authentication method designed for service-to-service communication, CLI tools, and integrations where JWT token management is impractical.
When to Use API Keys
| Use Case | Recommended Method | Reason |
|---|
| Browser applications | JWT (access/refresh tokens) | Token rotation, user session management |
| Mobile apps | JWT (access/refresh tokens) | Built-in token refresh, user experience |
| Service-to-service calls | API Keys | No token refresh needed, long-lived credentials |
| CLI tools | API Keys | Easy configuration, no session management |
| Webhooks | API Keys | Static credentials for incoming requests |
| Third-party integrations | API Keys | Simple credential sharing |
| IoT devices | API Keys | Limited token handling capabilities |
API keys follow a structured format:
sb_sk_<account_code>_<random_32_characters>
Example: sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Components:
sb_sk - SnackBase Secret Key prefix (identifies the key type)
AB1234 - Account code (human-readable account identifier)
a1b2c3...o5p6 - 32-character cryptographically secure random string
API Key Authentication Flow
┌──────────────┐
│ Admin creates │
│ API key via │
│ UI or API │
└──────┬───────┘
│
▼
┌─────────────────────────────────┐
│ POST /api/v1/api-keys/ │
│ { │
│ "name": "Production Service", │
│ "description": "Backend API" │
│ } │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ System generates API key: │
│ - Format: sb_sk_<code>_<random> │
│ - Store SHA-256 hash in DB │
│ - Return full key (ONCE ONLY) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Admin stores key securely: │
│ - Environment variable │
│ - Secret manager (Vault, AWS) │
│ - Configuration file (secured) │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Service uses API key: │
│ Authorization: Bearer sb_sk_... │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Server validates: │
│ 1. Extract key from header │
│ 2. Hash with SHA-256 │
│ 3. Lookup hash in database │
│ 4. Check not revoked │
│ 5. Get associated user/account │
└──────┬──────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Request processed with user │
│ context from API key │
└─────────────────────────────────┘
API Key Data Model
# API Key in Database
{
"id": "ak_abc123xyz", # Key ID (public)
"key_hash": "<SHA-256 hash>", # Hashed API key (never plaintext)
"account_id": "550e8400-...", # Associated account
"account_code": "AB1234", # Human-readable account code
"created_by": "usr_abc123", # User who created the key
"name": "Production Service", # Human-readable name
"description": "Backend API for prod", # Optional description
"last_used_at": "2026-01-17T15:45:00Z", # Last authentication timestamp
"is_revoked": false, # Revocation status
"created_at": "2026-01-01T00:00:00Z",
"revoked_at": null, # Set when revoked
"revoked_by": null # User who revoked it
}
Security Properties:
- SHA-256 Hashing: Keys are hashed before storage (plaintext never persisted)
- Single-User Association: Each key is linked to one user in one account
- Immediate Revocation: Keys can be revoked instantly
- Audit Trail: Tracks creation, last used, and revocation
- Account Scoping: Keys are automatically scoped to their account
Authentication Comparison
| Feature | API Keys | JWT Tokens |
|---|
| Use Case | Service-to-service, CLI, webhooks | Browser, mobile apps |
| Lifetime | Indefinite (until revoked) | Access: 1 hour, Refresh: 7 days |
| Storage | Server-side (hash) | Client-side (localStorage/cookie) |
| Visibility | Full key shown only at creation | Tokens visible in responses |
| Rotation | Manual (create new, revoke old) | Automatic (on refresh) |
| Revocation | Immediate | On refresh or logout |
| User Context | Single user per key | Can include user, role, account |
| Security | SHA-256 hashed at rest | Signed, verifiable signature |
| Token Management | Not applicable | Required (refresh mechanism) |
| Session Support | None (stateless) | Yes (with refresh tokens) |
Creating API Keys
Via API:
# Create API key
curl -X POST http://localhost:8000/api/v1/api-keys/ \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Service",
"description": "Backend API for production environment"
}'
# Response (only time full key is shown)
{
"id": "ak_abc123xyz",
"name": "Production Service",
"description": "Backend API for production environment",
"key": "sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", # SAVE THIS!
"account_id": "AB1234",
"created_at": "2026-01-17T10:30:00Z"
}
Via Admin UI:
- Navigate to Settings → API Keys
- Click “Create API Key”
- Enter name and description
- Copy the displayed key (shown only once)
- Store securely in your application
Using API Keys
API keys use the standard Authorization: Bearer header:
# Using API key for authentication
curl -X GET http://localhost:8000/api/v1/posts \
-H "Authorization: Bearer sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
# Creating a record
curl -X POST http://localhost:8000/api/v1/posts \
-H "Authorization: Bearer sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello World",
"content": "Created with API key"
}'
In Code:
import requests
# Configure API key
API_KEY = "sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
headers = {"Authorization": f"Bearer {API_KEY}"}
# Make requests
response = requests.get(
"http://localhost:8000/api/v1/posts",
headers=headers
)
// Using fetch
const API_KEY = "sb_sk_AB1234_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6";
const response = await fetch("http://localhost:8000/api/v1/posts", {
headers: {
"Authorization": `Bearer ${API_KEY}`
}
});
Managing API Keys
List API Keys:
curl -X GET http://localhost:8000/api/v1/api-keys/ \
-H "Authorization: Bearer <jwt_token>"
Response (metadata only, no full keys):
{
"items": [
{
"id": "ak_abc123xyz",
"name": "Production Service",
"description": "Backend API",
"created_at": "2026-01-17T10:30:00Z",
"last_used_at": "2026-01-17T15:45:00Z",
"is_revoked": false
}
]
}
Revoke API Key:
curl -X POST http://localhost:8000/api/v1/api-keys/ak_abc123xyz/revoke \
-H "Authorization: Bearer <jwt_token>"
Get Key Details:
curl -X GET http://localhost:8000/api/v1/api-keys/ak_abc123xyz \
-H "Authorization: Bearer <jwt_token>"
Security Best Practices
1. Secure Storage:
# ✅ Good: Environment variable
import os
API_KEY = os.environ.get("SNACKBASE_API_KEY")
# ✅ Good: Secret manager
import boto3
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='snackbase/api_key')
API_KEY = response['SecretString']
# ❌ Bad: Hardcoded in source
API_KEY = "sb_sk_AB1234_..." # NEVER do this
# ❌ Bad: Committed to version control
# .env files should be in .gitignore
2. Key Rotation:
# 1. Create new key
new_key_response=$(curl -s -X POST http://localhost:8000/api/v1/api-keys/ \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{"name": "Rotated Key"}')
# 2. Extract key
new_key=$(echo "$new_key_response" | jq -r '.key')
# 3. Update application configuration
export SNACKBASE_API_KEY="$new_key"
# 4. Verify new key works
curl -H "Authorization: Bearer $new_key" http://localhost:8000/api/v1/auth/me
# 5. Revoke old key
curl -X POST http://localhost:8000/api/v1/api-keys/ak_old_key/revoke \
-H "Authorization: Bearer <jwt_token>"
3. Scoping and Naming:
{
"name": "Production - Payment Service",
"description": "Used by payment processing service in production environment"
}
Use descriptive names to identify:
- Environment (Production, Staging, Development)
- Service (Payment Service, Webhook Handler, CLI)
- Purpose (Backup Job, Monitoring Integration)
4. Monitoring and Auditing:
# Check for unused keys
curl -X GET http://localhost:8000/api/v1/api-keys/ \
-H "Authorization: Bearer <jwt_token>" | \
jq '.items[] | select(.last_used_at == null)'
# Check for old keys (not used in 90 days)
# Implement automated monitoring and alerting
5. Revocation on Compromise:
If an API key is accidentally exposed (committed to repo, logged, etc.):
- Immediately revoke the compromised key
- Create a replacement key
- Update all services using the old key
- Investigate potential unauthorized access
- Review audit logs for suspicious activity
API Key vs User Permissions
API keys inherit the permissions of the user who created them:
# Admin user creates API key
# → API key has admin permissions
# Viewer user creates API key
# → API key has viewer permissions
# Key permissions are tied to creating user's role
This means:
- Create dedicated service users with minimal required permissions
- Don’t use personal admin accounts to create production API keys
- Regularly audit which users have created API keys
Common Patterns
Service Authentication:
# Backend service calling SnackBase
import os
import requests
API_KEY = os.environ["SNACKBASE_API_KEY"]
headers = {"Authorization": f"Bearer {API_KEY}"}
def create_post(title, content):
response = requests.post(
"http://localhost:8000/api/v1/posts",
headers=headers,
json={"title": title, "content": content}
)
return response.json()
CLI Tool:
# Configure CLI with API key
snackbase config set api-key sb_sk_AB1234_...
# CLI uses stored key for all commands
snackbase posts list
snackbase posts create --title "Hello"
Webhook Handler:
# Webhook endpoint validates API key
from fastapi import Header, HTTPException
async def webhook_handler(
authorization: str = Header(...)
):
if not authorization.startswith("Bearer sb_sk_"):
raise HTTPException(401, "Invalid API key")
# Process webhook
Security Features
Password Hashing (Argon2id)
SnackBase uses Argon2id, the OWASP-recommended password hashing algorithm:
import argon2
# Password hasher configuration
hasher = argon2.PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # Memory in KiB (64 MB)
parallelism=4, # Number of threads
hash_len=32, # Hash length
salt_len=16 # Salt length
)
# Hash password
password_hash = hasher.hash("SecurePass123!")
# $argon2id$v=19$m=65536,t=3,p=4$...
# Verify password (timing-safe)
is_valid = hasher.verify(password_hash, "SecurePass123!")
Password Requirements
Default password requirements (configurable):
| Requirement | Minimum |
|---|
| Length | 8 characters |
| Uppercase | 1 character |
| Lowercase | 1 character |
| Number | 1 digit |
| Special character | 1 character |
Token Expiration
| Token Type | Default Lifetime | Configurable Via |
|---|
| Access Token | 1 hour | SNACKBASE_ACCESS_TOKEN_EXPIRE_MINUTES |
| Refresh Token | 7 days | SNACKBASE_REFRESH_TOKEN_EXPIRE_DAYS |
Best Practices
1. Token Storage
For Web Applications:
// ✅ Recommended: HttpOnly cookies for refresh tokens
// Set-Cookie: refresh_token=<token>; HttpOnly; Secure; SameSite=Strict
// ⚠️ Acceptable: localStorage for access token only
localStorage.setItem("access_token", token);
// ❌ Avoid: localStorage for refresh tokens
localStorage.setItem("refresh_token", token); // Vulnerable to XSS
2. Token Refresh
Implement proactive token refresh:
// Refresh token 5 minutes before expiration
const token = parseJwt(access_token);
const expiresAt = token.exp * 1000;
const now = Date.now();
const refreshBefore = 5 * 60 * 1000; // 5 minutes
if (expiresAt - now < refreshBefore) {
await refreshToken();
}
3. Handle Token Expiration
// Axios interceptor for automatic token refresh
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Access token expired
try {
const newToken = await refreshToken();
// Retry original request
return axios.request(error.config);
} catch {
// Refresh token expired - redirect to login
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
4. Logout Properly
async function logout() {
// Clear tokens from storage
localStorage.removeItem("access_token");
document.cookie = "refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
// Call backend logout to revoke refresh token
await axios.post("/api/v1/auth/logout");
// Redirect to login
window.location.href = "/login";
}
5. Use HTTPS in Production
Never send tokens over unencrypted connections:
# ❌ Development only
http://localhost:8000
# ✅ Production
https://yourdomain.com
Summary
| Concept | Key Takeaway |
|---|
| User Identity | Defined by (email, account_id) tuple |
| Account Registration | Creates new tenant with UUID primary key and XX#### display code |
| User Registration | Creates user within specific account, email unique per account |
| Email Verification | Required for login, tokens expire in 1 hour, single-use |
| Login Flow | Resolve account → Find user → Check verification → Verify password → Issue JWT |
| Token Management | Access token (1 hour) + Refresh token (7 days) with true rotation |
| OAuth Authentication | Redirect → Authorize → Callback → Exchange tokens → Create/update user |
| SAML Authentication | SSO request → IdP → ACS response → Verify → Create/update user |
| Multi-Account Users | Same email can exist in multiple accounts with different passwords |
| Security | Argon2id hashing, timing-safe comparison, token rotation, HTTPS required |
| Configuration | Hierarchical: system-level defaults → account-level overrides |