Skip to content

Authentication & Authorization

Subscribe Flow supports dual authentication: API keys for programmatic access and magic link sessions for the dashboard. All resources are organization-scoped.

Authentication Flow

flowchart TD
    Start([Request]) --> CheckAuth{Auth Method?}

    CheckAuth -->|X-API-Key header| ValidateKey[Validate API Key]
    CheckAuth -->|sf_session cookie| ValidateSession[Validate JWT Session]
    CheckAuth -->|token query param| ValidateToken[Validate Preference Token]
    CheckAuth -->|None| Reject[401 Unauthorized]

    ValidateKey --> CheckKeyDB{Key in DB?}
    CheckKeyDB -->|Yes| CheckKeyActive{Active?}
    CheckKeyDB -->|No| Reject
    CheckKeyActive -->|Yes| ResolveOrgKey[Resolve org from API key]
    CheckKeyActive -->|No| Reject
    ResolveOrgKey --> AdminAccess[Access Granted]

    ValidateSession --> CheckJWTSig{Valid Signature?}
    CheckJWTSig -->|Yes| CheckJWTExp{Not Expired?}
    CheckJWTSig -->|No| Reject
    CheckJWTExp -->|Yes| CheckOrgHeader{X-Organization-Id?}
    CheckJWTExp -->|No| Reject
    CheckOrgHeader -->|Yes| CheckMembership{User is member?}
    CheckOrgHeader -->|No| Reject403[403 Missing Org]
    CheckMembership -->|Yes| SessionAccess[Access Granted]
    CheckMembership -->|No| Reject403

    ValidateToken --> CheckTokenDB{Token valid?}
    CheckTokenDB -->|Yes| CheckTokenExp{Not Expired?}
    CheckTokenDB -->|No| Reject
    CheckTokenExp -->|Yes| PrefAccess[Preference Access]
    CheckTokenExp -->|No| Reject

    AdminAccess --> Process[Process Request]
    SessionAccess --> Process
    PrefAccess --> Process
    Process --> Response([200 OK])
    Reject --> ErrorResponse([401/403 Error])
    Reject403 --> ErrorResponse

    style AdminAccess fill:#4ade80
    style SessionAccess fill:#60a5fa
    style PrefAccess fill:#a78bfa
    style Reject fill:#f87171
    style Reject403 fill:#f87171
    style Process fill:#fbbf24

Authentication Methods

1. API Key (Programmatic Access)

Use Case: Server-to-server integration, SDK usage, CI/CD pipelines

Header:

HTTP
X-API-Key: sf_live_abc123...

Properties:

  • Organization-scoped: each API key belongs to exactly one organization
  • Long-lived (no expiration date by default)
  • Can be rotated (generates new key, old key invalidated)
  • Scope-based permissions (e.g. subscribers:read, tags:write)
  • Rate limiting per API key

Organization Context Resolution:

The organization is resolved automatically from the API key itself. No additional header required.

Security:

  • API keys are stored hashed in the database (SHA-256 with prefix for lookup)
  • Key prefix (sf_live_ / sf_test_) is stored for identification
  • Only the full key is shown once at creation time

Use Case: Admin dashboard login, browser-based access

This is a passwordless authentication flow using magic links sent via email.

sequenceDiagram
    participant U as User (Browser)
    participant API as Subscribe Flow API
    participant E as Email (Resend)

    U->>API: POST /auth/magic-link {email}
    API->>API: Generate token + hash
    API->>API: Store hash in magic_link_tokens
    API->>E: Send email with link
    API->>U: 200 OK (always, anti-enumeration)

    U->>U: Click link in email
    U->>API: GET /auth/verify?token=xxx
    API->>API: Hash token, look up in DB
    API->>API: Check not expired, not used
    API->>API: Mark token as used
    API->>API: Get-or-create User
    API->>API: Create JWT session token
    API-->>U: Set-Cookie: sf_session=JWT (HttpOnly, 7 days)
    API->>U: 200 OK {user_id, email, organizations}

Session Cookie Properties:

Property Value
Cookie name sf_session
HttpOnly Yes (not accessible via JavaScript)
SameSite Lax
Secure Yes (in production)
Max-Age 7 days
Domain Configured via SESSION_COOKIE_DOMAIN

Cross-Subdomain Auth

In production, SESSION_COOKIE_DOMAIN is set to .subscribeflow.net so that the session cookie is shared across all subdomains (e.g. app.subscribeflow.net, api.subscribeflow.net). For local development, leave this variable empty.

JWT Payload:

JSON
1
2
3
4
5
6
{
  "sub": "user_uuid",
  "email": "user@example.com",
  "exp": 1234567890,
  "iat": 1234564290
}

Organization Context Resolution:

Session-based requests must include the X-Organization-Id header to specify which organization the request targets. The system verifies the user is a member of that organization.

HTTP
X-Organization-Id: 660e8400-e29b-41d4-a716-446655440000

3. Preference Token (Subscriber Self-Service)

Use Case: Preference Center (self-service email management)

Query Parameter:

Text Only
GET /preference-center?token=eyJhbGciOiJIUzI1NiIs...

Properties:

  • JWT containing subscriber ID and organization ID
  • Generated via POST /api/v1/subscribers/{id}/token
  • Typically included in email unsubscribe links
  • Access limited to the subscriber's own preference data

Dual Auth Dependency

All /api/v1/* endpoints accept either authentication method:

  1. API Key (X-API-Key header) -- org resolved from key
  2. Session Cookie (sf_session cookie + X-Organization-Id header) -- org resolved from header after membership check

The FastAPI dependency get_current_organization tries API key first, then falls back to session auth. If neither is present, returns 401 Unauthorized.

Session-Aware API Key Resolution

When a session cookie is present, the get_api_key() dependency returns None instead of raising 401 Unauthorized. This allows all require_scope dependencies to fall through to session-based authentication. Without this fix, requests from the dashboard (which carry a session cookie but no API key) would fail on scope-protected endpoints.

Text Only
1
2
3
Request mit Session Cookie → get_api_key() returns None → require_scope() akzeptiert Session Auth → OK
Request mit API Key        → get_api_key() returns APIKey → require_scope() prüft Scopes → OK
Request ohne beides        → 401 Unauthorized

Race Condition Fix (Frontend)

The tryRestoreSession function in the frontend skips the session restore API call when no stored user exists in local state. This prevents a race condition where multiple components could simultaneously attempt to restore a non-existent session on initial page load.

Roles & Permissions

Organization Roles

Role Description Capabilities
Owner Organization creator/admin Full access including billing and settings
Member Team member API access to subscribers, tags, templates, etc.

Role-Restricted Endpoints

Endpoint Group Owner Member
Subscribers, Tags, Templates Yes Yes
Campaigns, Triggers, Webhooks Yes Yes
API Keys Yes Yes
Billing (/api/v1/billing/*) Yes No
Email Settings (/api/v1/settings/email) Yes No
Organization Management Yes No

API Key Scopes

API keys can be further restricted with scopes:

Scope Description
subscribers:read Read subscriber data
subscribers:write Create and update subscribers
subscribers:delete Delete subscribers
tags:read Read tags
tags:write Create/update/delete tags
webhooks:manage Manage webhook endpoints
* All permissions (admin)

Rate Limiting

Auth Method Limit
API Key 1000 requests/hour
Session Cookie 100 requests/hour
Preference Token 10 requests/minute

Security Best Practices

  1. API Keys:

    • Never commit to Git or expose in client-side code
    • Store in environment variables
    • Rotation every 90 days recommended
    • Use minimal scopes (principle of least privilege)
    • Regenerate immediately upon leak
  2. Session Cookies:

    • HttpOnly prevents XSS token theft
    • SameSite=Lax prevents CSRF
    • Secure flag enforced in production (HTTPS only)
    • 7-day expiry limits session lifetime
  3. Magic Links:

    • Single-use (marked as used after verification)
    • Short-lived (15 minutes)
    • Anti-enumeration: always returns success response
    • Rate limiting recommended on /auth/magic-link
  4. Preference Tokens:

    • Only distributed via email
    • HTTPS-only
    • Scoped to single subscriber