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 | |
|---|---|
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
2. Magic Link + Session Cookie (Dashboard Access)¶
Use Case: Admin dashboard login, browser-based access
This is a passwordless authentication flow using magic links sent via email.
Magic Link Flow¶
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 | |
|---|---|
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 | |
|---|---|
3. Preference Token (Subscriber Self-Service)¶
Use Case: Preference Center (self-service email management)
Query Parameter:
| Text Only | |
|---|---|
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:
- API Key (
X-API-Keyheader) -- org resolved from key - Session Cookie (
sf_sessioncookie +X-Organization-Idheader) -- 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 | |
|---|---|
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¶
-
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
-
Session Cookies:
- HttpOnly prevents XSS token theft
- SameSite=Lax prevents CSRF
- Secure flag enforced in production (HTTPS only)
- 7-day expiry limits session lifetime
-
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
-
Preference Tokens:
- Only distributed via email
- HTTPS-only
- Scoped to single subscriber