Skip to main content
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:
FeatureDescription
Account-Scoped UsersUsers belong to specific accounts
Multi-Account SupportSame email can exist in multiple accounts
Per-Account PasswordsDifferent passwords per (email, account) tuple
JWT TokensAccess tokens (1 hour) + Refresh tokens (7 days)
Token RotationRefresh token rotation on each use with revocation
API KeysService authentication with hashed keys
Email VerificationRequired for login, tokens expire in 1 hour
Multi-ProviderSupport for Password, OAuth, and SAML providers
Identity LinkingLink local accounts with external provider identities
Timing-Safe ComparisonPassword verification resistant to timing attacks
Hierarchical ConfigSystem-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.

Account ID Format

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:
✅ ALLOWED:
Account AB1001: [email protected]
Account XY2048: [email protected]  (Same email, different account)

❌ NOT ALLOWED:
Account AB1001: [email protected]
Account AB1001: [email protected]  (Duplicate within 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 TypeLifetimePurposeStorageDatabase
Access Token1 hourAPI requestslocalStorage/memoryNo
Refresh Token7 daysGet new access tokenHttpOnly cookie or localStorageYes

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:
  1. Old refresh token is marked as revoked in database
  2. New refresh token is generated and stored (hash)
  3. Old token cannot be used again (returns 401 if attempted)
  4. 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

ProviderDescription
GoogleGoogle Account login
GitHubGitHub account login
MicrosoftMicrosoft / Azure AD login
AppleSign 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

ProviderDescription
OktaOkta Identity Cloud SSO
Azure ADMicrosoft Azure Active Directory
GenericAny 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 CaseRecommended MethodReason
Browser applicationsJWT (access/refresh tokens)Token rotation, user session management
Mobile appsJWT (access/refresh tokens)Built-in token refresh, user experience
Service-to-service callsAPI KeysNo token refresh needed, long-lived credentials
CLI toolsAPI KeysEasy configuration, no session management
WebhooksAPI KeysStatic credentials for incoming requests
Third-party integrationsAPI KeysSimple credential sharing
IoT devicesAPI KeysLimited token handling capabilities

API Key Format

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

FeatureAPI KeysJWT Tokens
Use CaseService-to-service, CLI, webhooksBrowser, mobile apps
LifetimeIndefinite (until revoked)Access: 1 hour, Refresh: 7 days
StorageServer-side (hash)Client-side (localStorage/cookie)
VisibilityFull key shown only at creationTokens visible in responses
RotationManual (create new, revoke old)Automatic (on refresh)
RevocationImmediateOn refresh or logout
User ContextSingle user per keyCan include user, role, account
SecuritySHA-256 hashed at restSigned, verifiable signature
Token ManagementNot applicableRequired (refresh mechanism)
Session SupportNone (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:
  1. Navigate to Settings → API Keys
  2. Click “Create API Key”
  3. Enter name and description
  4. Copy the displayed key (shown only once)
  5. 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.):
  1. Immediately revoke the compromised key
  2. Create a replacement key
  3. Update all services using the old key
  4. Investigate potential unauthorized access
  5. 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):
RequirementMinimum
Length8 characters
Uppercase1 character
Lowercase1 character
Number1 digit
Special character1 character

Token Expiration

Token TypeDefault LifetimeConfigurable Via
Access Token1 hourSNACKBASE_ACCESS_TOKEN_EXPIRE_MINUTES
Refresh Token7 daysSNACKBASE_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

ConceptKey Takeaway
User IdentityDefined by (email, account_id) tuple
Account RegistrationCreates new tenant with UUID primary key and XX#### display code
User RegistrationCreates user within specific account, email unique per account
Email VerificationRequired for login, tokens expire in 1 hour, single-use
Login FlowResolve account → Find user → Check verification → Verify password → Issue JWT
Token ManagementAccess token (1 hour) + Refresh token (7 days) with true rotation
OAuth AuthenticationRedirect → Authorize → Callback → Exchange tokens → Create/update user
SAML AuthenticationSSO request → IdP → ACS response → Verify → Create/update user
Multi-Account UsersSame email can exist in multiple accounts with different passwords
SecurityArgon2id hashing, timing-safe comparison, token rotation, HTTPS required
ConfigurationHierarchical: system-level defaults → account-level overrides