Skip to content

Authentication Architecture

Multi-site authentication system with one API backend serving a main site and multiple satellite sites.

  • Main Site: gigmanager.local (production: example.com) - Account owner, payments, full access
  • Satellite Sites: Customer domains - Team members, limited permissions
  • API Backend: api.gigmanager.local (production: api.example.com) - Single source of truth
  • Mobile Apps: Future - Same as satellite permissions
  1. accessToken (JWT)

    • Short-lived (15 minutes)
    • Sent in Authorization: Bearer header
    • Validated stateless (signature verification only)
    • Contains: userId, permissions, expiry
  2. sessionId (UUID)

    • Long-lived (30 days)
    • Stored in database/Redis
    • Used only for refresh operations
    • Links to user account and permissions
  • accessToken: Memory (lost on page refresh)
  • sessionId: httpOnly cookie (automatic, secure from XSS)
  • Permissions: Full (payments, account management, team management)
  • CORS: credentials: 'include' required
  • accessToken: Memory or localStorage
  • sessionId: localStorage
  • Permissions: Limited (data entry, reports only - no payments/account settings)
  • CORS: Headers only, no credentials
POST /api/login
Body: { email, password, source: "main" }
Credentials: include
Server Response:
Set-Cookie: sessionId=xyz; HttpOnly; Secure; SameSite=Lax
Body: { accessToken }
Client Storage:
accessToken → memory
sessionId → automatic (cookie)
POST /api/login
Body: { email, password, source: "satellite" }
Server Response:
Body: { accessToken, sessionId }
Client Storage:
accessToken → localStorage or memory
sessionId → localStorage
POST /api/refresh
Credentials: include (sessionId cookie sent automatically)
Server:
Reads req.cookies.sessionId
Validates session in database
Returns: { accessToken }
Client:
Updates accessToken in memory
POST /api/refresh
Header: X-Session-ID: {sessionId from localStorage}
Server:
Reads req.headers['x-session-id']
Validates session in database
Returns: { accessToken }
Client:
Updates accessToken in localStorage
GET /api/data
Header: Authorization: Bearer {accessToken}
Server:
Validates JWT signature (stateless)
Extracts userId/permissions from claims
Processes request
POST /api/subscription/create
Header: Authorization: Bearer {accessToken}
Cookie: sessionId (automatic)
Server:
1. Validates accessToken
2. Checks req.cookies.sessionId exists (403 if not)
3. Validates session.source === "main"
4. Validates session.permissions.payments === true
5. Processes payment via Stripe
{
sessionId: "uuid-v4",
userId: "user_id",
accountId: "main_account_id", // Links team members to paying account
source: "main" | "satellite",
permissions: {
payments: boolean, // Main site only
accountSettings: boolean, // Main site only
teamManagement: boolean, // Main site only
dataEntry: boolean, // Both
viewReports: boolean // Both
},
createdAt: timestamp,
lastUsed: timestamp,
expiresAt: timestamp
}
  • httpOnly cookies prevent XSS token theft
  • SameSite=Lax prevents CSRF attacks
  • Payment operations require cookie (can’t be triggered from satellites)
  • Re-authentication required for sensitive changes
  • Limited permissions in session (no payments, no account changes)
  • Session revocation possible server-side
  • XSS risk accepted (localStorage) but damage limited by permissions
  • Same-origin policy prevents cross-site token theft

Main site token (XSS on gigmanager.local):

  • Attacker can use httpOnly cookie via requests (can’t steal it)
  • Can perform user actions until session revoked
  • Can’t exfiltrate sessionId itself
  • Session can be killed server-side

Satellite token (XSS on satellite or localStorage theft):

  • Attacker gains limited permissions only
  • Cannot manage payments or account
  • Cannot add/remove team members
  • Session can be revoked server-side

All payments happen on main site only.

From Satellite:

  1. User clicks “Subscribe” or “Upgrade”
  2. Redirect to main site: https://gigmanager.local/subscribe?return={satellite-url}
  3. User completes payment on main site (secure cookie auth)
  4. Redirect back to satellite site after completion

Stripe Integration:

  • Main site creates Stripe Checkout Session (requires cookie auth)
  • User completes payment on Stripe’s domain
  • Stripe webhook notifies backend
  • Backend updates subscription status
  • No credit card data touches our servers
  • 99% of requests validate JWT stateless (microsecond latency)
  • Database lookup only on refresh (every 15 min per user)
  • Redis session storage for fast lookups
  • Horizontal scaling trivial (stateless validation)
  • Payment operations isolated to most secure domain
  • Defense in depth (different auth methods, different permissions)
  • Revocation capability when needed
  • XSS damage limited by permission scoping
  • Works across unrelated domains (no CORS cookie issues)
  • Mobile apps use same pattern as satellites
  • Each frontend uses authentication that works for its context
  • Single API serves all clients
  • Each frontend has one auth pattern (not mixing methods)
  • Clear separation of concerns
  • Standard patterns (JWT, httpOnly cookies, localStorage)
  • Minimal CSRF concerns (SameSite + no sensitive cookies on satellites)
  • Node.js serves HTTPS directly with local certificates
  • Hosts file maps local domains
  • Both tokens visible for debugging
  • Nginx/reverse proxy terminates HTTPS
  • Node.js serves HTTP internally
  • Let’s Encrypt handles certificates
  • Same code, environment variable switches behavior
  • Redis for session storage (fast, scalable)
  • Regular cleanup of expired sessions
  • Session metadata for anomaly detection (IP, user agent)
  • Audit log for sensitive operations
  • accessToken: 15 minutes (balance between performance and security)
  • sessionId: 30 days (user convenience, can be shorter for high-security)
  • Configurable per environment

Not chosen because:

  • Can’t revoke compromised tokens
  • Can’t force logout or ban users mid-session
  • Can’t update permissions without re-login
  • Payment system requires revocation capability

Would be suitable for:

  • Read-only applications
  • No payment processing
  • Acceptable to wait for token expiry on security events