Skip to main content
SnackBase provides a comprehensive authentication system designed for multi-tenant applications. This guide explains authentication flows, token management, 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
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.com/api/v1/auth/login
{
  "email": "[email protected]",
  "password": "Password1!"
}
Option 2: Account in Request Body
POST https://snackbase.com/api/v1/auth/login
{
  "account": "acme-corp",  // Account slug
  "email": "[email protected]",
  "password": "Password1!"
}

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