Auth0 setup — comprehensive operator runbook
This is the canonical setup guide for AGENTS hard rules 52-55 (the Phase 5+ auth refactor). Pair with alphaswarm_docs/auth0-actions.md for the JS Action bodies that go in the Auth0 Dashboard.
The platform supports three deployment shapes:
- Local-first dev:
ALPHASWARM_AUTH_PROVIDER=local, no Auth0 tenant needed. Everything below is skipped. - Single-tenant B2C: one Auth0 tenant per env, individual users sign up via Universal Login + social connections. Organizations is OFF (or "Allow individual logins" if you want both modes).
- Multi-tenant B2B: same Auth0 tenant per env, institutional customers attach via Auth0 Organizations. Each Organization has its own branded login + Enterprise connection.
The same backend serves all three; the difference is purely the
Auth0 configuration + the ALPHASWARM_AUTH_* env vars.
1. Tenants
One Auth0 tenant per AlphaSwarm environment. Three tenants per AGENTS rule:
| Env | Auth0 tenant | Custom domain | Issuer URL in ALPHASWARM_AUTH_OIDC_ISSUER |
|---|---|---|---|
| dev | alphaswarm-dev | auth.dev.alpha-swarm.ai | https://auth.dev.alpha-swarm.ai/ |
| stage | alphaswarm-stage | auth.stage.alpha-swarm.ai | https://auth.stage.alpha-swarm.ai/ |
| prod | alphaswarm-prod | auth.alpha-swarm.ai | https://auth.alpha-swarm.ai/ |
Custom domains stabilise the issuer URL so changing Auth0 tenants
later is non-breaking. Without a custom domain the issuer is
https://alphaswarm-prod.us.auth0.com/ and every existing JWT cache /
revocation token has to be invalidated on rebrand.
Never share Auth0 tenants across envs — Auth0 charges per MAU per tenant, but the security boundary is more important than the cost arithmetic.
2. API resource server
One API record per tenant — the AlphaSwarm backend.
| Field | Value (prod example) |
|---|---|
| Name | alphaswarm-api |
| Identifier | https://api.alpha-swarm.ai/ |
| Signing algorithm | RS256 |
| Allow Skipping User Consent | ON |
| Allow Offline Access | ON |
| Token expiration (seconds) | 86400 (24h ceiling — per-app overrides win) |
| Token expiration for browser flows (seconds) | 7200 (2h SPA ceiling) |
Enable RBAC:
- Settings → "Enable RBAC" → ON
- Settings → "Add Permissions in the Access Token" → ON
Define every permission AlphaSwarm uses (Permissions tab):
read:portfolio Read portfolio positions / PnL / risk
write:portfolio Mutate portfolio config
read:strategy Read strategy specs / backtest history
write:strategy Author / edit strategies
deploy:strategy Promote a strategy to live trading
kill_switch:execute Engage the global kill switch
trade:execute Submit live or paper orders
trade:live Bypass the paper-only guard
read:mcp:data Invoke the Data MCP tools
write:mcp:data Mutate via Data MCP (e.g. namespace policy edits)
read:mcp:codebase Invoke the Codebase MCP tools
write:mcp:codebase Apply code edits via Codebase MCP (rarely granted)
run:agent Spawn an AgentRuntime
admin:tenant Org-admin powers (invites, IdP config, billing)
admin:cluster Bypass resource filter; superadmin-only
manage:broker_credentials Read/write broker credentials at org scope
read:logs Required for the Auth0 Management API M2M client
Add Token Exchange:
- API → Settings → "Token Exchange" → ON (required for
alphaswarm-agent-brokerto use RFC 8693).
3. Applications
Five application records per tenant:
| Record | Type | Grants | Token TTL | Notes |
|---|---|---|---|---|
alphaswarm-spa | Single Page Application | authorization_code + refresh_token | access 15m, ID 10m | Refresh-token rotation ON, absolute lifetime 24h |
alphaswarm-cli | Native | urn:ietf:params:oauth:grant-type:device_code + refresh_token | access 60m | Rotation ON, absolute 30d, inactivity 7d. "Business Users" mode so Device Code stays compatible with Orgs |
alphaswarm-backend-m2m | M2M | client_credentials | 24h | For internal service-to-service + Auth0 Management API |
alphaswarm-action-callback-m2m | M2M | client_credentials | 5m | Used inside Auth0 Actions for /_internal/auth0/sync |
alphaswarm-agent-broker | M2M | client_credentials + urn:ietf:params:oauth:grant-type:token-exchange | 5m | RFC 8693 delegated-agent-token minting |
3.1 alphaswarm-spa (SPA)
- Application URIs:
- Allowed callback URLs:
https://app.alpha-swarm.ai/auth/callback,http://localhost:3001/auth/callback - Allowed logout URLs:
https://app.alpha-swarm.ai/,http://localhost:3001/ - Allowed web origins:
https://app.alpha-swarm.ai,http://localhost:3001
- Allowed callback URLs:
- Refresh Token Rotation: ON
- Refresh Token Expiration: Absolute 24h
- Refresh Token Inactivity: 7d
- Idle Session Lifetime: 72h
- Maximum Session Lifetime: 168h (7d)
Frontend env vars (Vite):
VITE_AUTH_PROVIDER=auth0
VITE_AUTH0_DOMAIN=auth.alpha-swarm.ai # custom domain
VITE_AUTH0_SPA_CLIENT_ID=<alphaswarm-spa client_id>
VITE_AUTH0_AUDIENCE=https://api.alpha-swarm.ai/
VITE_AUTH0_SCOPE=openid profile email offline_access read:portfolio write:portfolio read:strategy write:strategy read:mcp:data
VITE_AUTH0_ORGANIZATION= # B2B only — pin to a single org
3.2 alphaswarm-cli (Native)
- Connections tab: enable the same DB / social connections as the SPA.
- Advanced Settings → Grant Types: enable
Device Code+Refresh Token. - "Business Users" mode (not "Organizations Required"); the Auth0 team's M2M-for-Orgs GA notes that Device Code is incompatible with the strict "Organizations Required" setting.
CLI env vars (operator's machine):
ALPHASWARM_CLI_OIDC_DOMAIN=auth.alpha-swarm.ai
ALPHASWARM_CLI_OIDC_CLIENT_ID=<alphaswarm-cli client_id>
ALPHASWARM_CLI_OIDC_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_CLI_OIDC_ORGANIZATION= # B2B: pin to a single org
The CLI fetches all three from /auth/config when not set, so most
operators don't need to copy-paste.
3.3 alphaswarm-backend-m2m (M2M)
- Authorise against:
alphaswarm-api(all permissions the backend needs to act on its own behalf).- Auth0 Management API (
read:users,update:users,delete:sessions,read:sessions,read:logs,read:connections,create:guardian_enrollment_tickets,delete:guardian_enrollments,create:user_tickets).
Backend env vars:
ALPHASWARM_AUTH_PROVIDER=auth0
ALPHASWARM_AUTH_OIDC_ISSUER=https://auth.alpha-swarm.ai/
ALPHASWARM_AUTH_OIDC_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_AUTH_OIDC_CLIENT_ID=<alphaswarm-spa client_id> # SPA client_id (for the SPA-targeted JWKS validation path)
ALPHASWARM_AUTH_OIDC_CLIENT_SECRET= # empty — SPAs are public clients
ALPHASWARM_AUTH0_MGMT_API_AUDIENCE=https://alphaswarm-prod.us.auth0.com/api/v2/
ALPHASWARM_AUTH0_MGMT_API_CLIENT_ID=<alphaswarm-backend-m2m client_id>
ALPHASWARM_AUTH0_MGMT_API_CLIENT_SECRET= # via CredentialResolver in prod; env in dev
ALPHASWARM_AUTH0_DPOP_ENABLED=true # SDK mixed-mode
ALPHASWARM_AUTH0_DPOP_REQUIRED=false # flip true after CLI + SPA migrate
ALPHASWARM_AUTH_M2M_ENABLED=true
ALPHASWARM_AUTH_M2M_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_AUTH_STEP_UP_ENABLED=true
ALPHASWARM_AUTH_STEP_UP_DEFAULT_MAX_AGE=180
3.4 alphaswarm-action-callback-m2m (M2M)
Same scopes as alphaswarm-backend-m2m but used INSIDE Auth0 Actions to
call /_internal/auth0/sync + /_internal/idp/sync-groups. The
Action body in auth0-actions.md shows how to
mint + cache the token.
3.5 alphaswarm-agent-broker (M2M for Token Exchange)
- Grants:
client_credentials+urn:ietf:params:oauth:grant-type:token-exchange. - Authorised APIs:
alphaswarm-apiwith scopesread:mcp:data,write:mcp:data,read:mcp:codebase,write:mcp:codebase. - Used ONLY by the Custom Token Exchange Profile body to mint delegated agent tokens.
Backend env vars:
ALPHASWARM_AUTH_AGENT_TOKEN_EXCHANGE_ENABLED=true
ALPHASWARM_AUTH_AGENT_BROKER_CLIENT_ID=<alphaswarm-agent-broker client_id>
ALPHASWARM_AUTH_AGENT_BROKER_CLIENT_SECRET= # via CredentialResolver in prod
ALPHASWARM_AUTH_AGENT_DELEGATION_TTL_SECONDS=300
4. Connections
Database connection (B2C)
- Default
Username-Password-Authenticationdatabase connection. - Password Strength: "Excellent" (NIST 800-63 compliant).
- Enable: "Disable Signups from Public Signup Page" if you want invite-only onboarding (B2B-heavy deployments).
Social connections (B2C)
- GitHub, Google (
google-oauth2). Both default to the standard Auth0 connection types — no extra config beyond the Client ID + Secret from the respective developer console.
Enterprise connections (B2B)
Configured per-org in :class:IdpConnectionRecord. Auth0 supports
SAML, ADFS, Azure AD (Entra), Google Workspace, PingFederate,
SiteMinder, Okta Workforce Identity, OneLogin, JumpCloud,
generic OIDC. The AlphaSwarm-side admin UI is
IdpGroupMappingEditor.
Each enterprise connection MUST:
- Sync the user's group claims (Azure
groups, Google's group claim, Oktagroups). The Actionalphaswarm-idp-group-syncreads them. - Map to a single AlphaSwarm Organization via the matching
:class:
IdpConnectionRecord.organization_id. Multiple orgs may use the same connection KIND (e.g. AcmeCorp Okta + Subsidiary Okta) but each is a separate record.
5. Organizations (B2B)
One Auth0 Organization per institutional tenant. Auth0 charges per Org per month on most tiers — budget accordingly.
| Setting | Value |
|---|---|
| Membership on Login | "Require Members to use this Organization" (strict B2B) |
| Allowed Connections | Only the org's enterprise connection(s) |
| Branding | Per-org logo + colors so users land on a branded login |
Use ?organization=org_xxx&login_hint=user@acme.com on /authorize
to skip the org-picker step. The SPA reads
VITE_AUTH0_ORGANIZATION to pin.
The post-login Action (alphaswarm-post-login) reads event.organization?.id
and injects it as https://alphaswarm.internal/org_id so the FastAPI
require_org dep can branch immediately.
6. Actions
Three Login-trigger Actions (in this order):
-
alphaswarm-post-login— JIT user upsert + custom claim injection. Body in auth0-actions.md ("Phase 7 post-login Action" section, extended by "Phase 8" addendum for step-up MFA). -
alphaswarm-idp-group-sync— reads external IdP group claims and posts to/_internal/idp/sync-groupsso the AlphaSwarm backend upserts matching Membership rows per the per-org IdpGroupMapping table. Body in auth0-actions.md ("Phase 6 — IdP group sync Action" section).
And one Custom Token Exchange Profile:
alphaswarm-agent-delegation— RFC 8693 minting for delegated agent tokens. Body in auth0-actions.md ("Phase 8 — Custom Token Exchange Profile" section).
7. Pre-User-Registration trigger
One Action to block disposable emails + verify B2B invites:
exports.onExecutePreUserRegistration = async (event, api) => {
const email = (event.user.email || "").toLowerCase();
const disposable = ["mailinator.com", "guerrillamail.com", "tempmail.org",
"10minutemail.com", "throwaway.email"];
const domain = email.split("@")[1];
if (!email) { api.access.deny("invalid_email", "email required"); return; }
if (disposable.includes(domain)) {
api.access.deny("disposable_email", "disposable email domains not allowed");
return;
}
// B2B invite verification — operator chooses how strict.
if (event.client.metadata?.flow === "b2b" && event.secrets.ALPHASWARM_BACKEND_URL) {
// Call /_internal/auth/preregister-check (operator adds this route
// if they want HMAC-based invite enforcement at registration time).
}
};
8. Log Streams
One Custom Webhook log stream per env:
| Field | Value |
|---|---|
| Type | Custom Webhook |
| Payload URL | https://api.alpha-swarm.ai/_internal/auth0/log-stream |
| Authorization | Bearer <secret> (matches ALPHASWARM_AUTH0_LOG_STREAM_SECRET) |
| Content Type | application/json |
| Custom Headers | (none beyond Authorization) |
| Filter | All events (the backend filters server-side) |
Operator generates the shared secret:
openssl rand -hex 32
…then sets it both in the Auth0 Dashboard webhook config AND in
the backend's ALPHASWARM_AUTH0_LOG_STREAM_SECRET env var. The HMAC
compare on _verify_authorization rejects any other value.
Optionally also wire native Datadog / Splunk / Elastic streams for the SIEM team — those are independent of the AlphaSwarm webhook.
9. Adaptive MFA
Security → Multi-factor Authentication → Adaptive MFA → ON.
| Risk level | Action | Why |
|---|---|---|
low | Allow (no MFA) | Normal session resumption |
medium | MFA challenge | Suspicious-but-not-definitive signals |
high | MFA challenge | Likely compromised |
Enabled MFA factors (Security → Multi-factor Authentication → Factors):
- OTP (TOTP) — always-on; required for every B2B user
- WebAuthn — recommended primary for B2B users
- Push (Auth0 Guardian app) — B2C convenience
- SMS — discouraged for B2B; allow as B2C fallback only
- Email OTP — convenient B2C fallback
- Recovery codes — always issue alongside any factor
The alphaswarm-post-login Action's Phase 8 addendum calls
api.multifactor.enable("any", { allowRememberBrowser: false })
when the SPA / CLI requests acr_values=http://schemas.openid.net/pape/policies/2007/06/multi-factor
on /authorize. This is the integration point for the backend's
require_step_up dep.
10. Env-var checklist (prod)
# IdP
ALPHASWARM_AUTH_PROVIDER=auth0
ALPHASWARM_AUTH_REQUIRED=true
ALPHASWARM_AUTH_ENFORCE=strict
ALPHASWARM_AUTH_OIDC_ISSUER=https://auth.alpha-swarm.ai/
ALPHASWARM_AUTH_OIDC_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_AUTH_OIDC_CLIENT_ID=<alphaswarm-spa client_id>
ALPHASWARM_AUTH_CLAIMS_NAMESPACE=https://alphaswarm.internal/
ALPHASWARM_AUTH_CLAIMS_NAMESPACE_ALIASES=https://alphaswarm/ # CSV; legacy reader
# Management API
ALPHASWARM_AUTH0_MGMT_API_AUDIENCE=https://alphaswarm-prod.us.auth0.com/api/v2/
ALPHASWARM_AUTH0_MGMT_API_CLIENT_ID=<alphaswarm-backend-m2m client_id>
ALPHASWARM_AUTH0_MGMT_API_CLIENT_SECRET= # via CredentialResolver
# M2M
ALPHASWARM_AUTH_M2M_ENABLED=true
ALPHASWARM_AUTH_M2M_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_AUTH_M2M_TOKEN_TTL_SECONDS=900
# DPoP
ALPHASWARM_AUTH0_DPOP_ENABLED=true
ALPHASWARM_AUTH0_DPOP_REQUIRED=false # flip true once SDK rolled out
ALPHASWARM_DPOP_ENFORCEMENT_ENABLED=false # per-route enforcement
# Step-up MFA (rule 52)
ALPHASWARM_AUTH_STEP_UP_ENABLED=true
ALPHASWARM_AUTH_STEP_UP_DEFAULT_MAX_AGE=180
# Auth0 Log Stream (rule 53)
ALPHASWARM_AUTH0_LOG_STREAM_SECRET=<openssl rand -hex 32>
ALPHASWARM_AUTH0_LOG_STREAM_MAX_AGE_SECONDS=86400
# Delegated agent tokens (rule 54)
ALPHASWARM_AUTH_AGENT_TOKEN_EXCHANGE_ENABLED=true
ALPHASWARM_AUTH_AGENT_BROKER_CLIENT_ID=<alphaswarm-agent-broker client_id>
ALPHASWARM_AUTH_AGENT_BROKER_CLIENT_SECRET= # via CredentialResolver
ALPHASWARM_AUTH_AGENT_DELEGATION_TTL_SECONDS=300
# B2B Entra (existing)
ALPHASWARM_AUTH_MSAL_B2B_ENABLED=true
# Tenancy
ALPHASWARM_TENANCY_DEFAULT_STRATEGY=hybrid
ALPHASWARM_TENANCY_RLS_ENFORCE=strict # was off; flip after Phase 5 verified
# MCP RFC conformance
ALPHASWARM_MCP_DATA_CANONICAL_URI=https://api.alpha-swarm.ai/mcp/data
ALPHASWARM_MCP_CODEBASE_CANONICAL_URI=https://api.alpha-swarm.ai/mcp/codebase
ALPHASWARM_MCP_REQUIRE_RFC8707=strict # was off
# Per-user OAuth wizard
ALPHASWARM_USER_OAUTH_ENABLED=true
# Audit
ALPHASWARM_AUTH_AUDIT_ENABLED=true
ALPHASWARM_AUTH_AUDIT_RETENTION_DAYS=365
11. CLI env vars (per operator)
ALPHASWARM_CLI_OIDC_DOMAIN=auth.alpha-swarm.ai
ALPHASWARM_CLI_OIDC_CLIENT_ID=<alphaswarm-cli client_id>
ALPHASWARM_CLI_OIDC_AUDIENCE=https://api.alpha-swarm.ai/
ALPHASWARM_CLI_OIDC_ORGANIZATION= # B2B: pin to a single org
# Headless / CI fallback (no keyring backend):
ALPHASWARM_CLI_AUTH_ALLOW_PLAINTEXT_FALLBACK=0
12. Rollout order
| Step | Action | Verification |
|---|---|---|
| 1 | Create dev tenant + apps + custom domain | /auth/config returns the tenant id |
| 2 | Backend up with ALPHASWARM_AUTH_ENFORCE=permissive | Existing routes still serve; 401 dashboard shows zero would-be denies |
| 3 | Flip ALPHASWARM_AUTH_ENFORCE=strict | Unauthenticated calls return 401 |
| 4 | Wire Auth0 log-stream webhook + Action triggers | Force a session-revoke in Dashboard; verify cleanup_for_user Celery row + audit row |
| 5 | Enable ALPHASWARM_AUTH_STEP_UP_ENABLED=true | Click kill-switch → MFA prompt; complete it; subsystems halt |
| 6 | Enable ALPHASWARM_AUTH_AGENT_TOKEN_EXCHANGE_ENABLED=true + create Profile | Trigger an agent that calls a DataMCP tool; verify act claim in /mcp/data response body + delegation JSON in audit |
| 7 | Enable ALPHASWARM_USER_OAUTH_ENABLED=true | /me/oauth-connections/providers returns the 5 providers |
| 8 | Enable BYOK broker credentials (run Alembic 0065) | Add an Alpaca paper key; smoke-test a paper trade |
| 9 | Enable RLS strict mode (ALPHASWARM_TENANCY_RLS_ENFORCE=strict) | Existing test workspace queries still work; cross-workspace fetches return zero rows |
| 10 | Enable MCP RFC 8707 strict mode | MCP calls with mis-audienced tokens return 401 + WWW-Authenticate header |
Each flip is independently reversible.
13. Reference docs
- alphaswarm_docs/auth0-actions.md — Action bodies + the Custom Token Exchange Profile setup.
- alphaswarm_docs/identity.md — the full identity stack.
- alphaswarm_docs/multi-tenancy.md — Organization → EntraTenantLink → User → Membership flow.
- alphaswarm_docs/credentials.md — how M2M + BYOK credentials flow through CredentialResolver.
- .cursor/rules/identity.mdc — the always-on identity-enforcement rule.
- .cursor/rules/auth-stepup-and-byok.mdc — Phase 5+ rules (52-55) scoped to the new module files.
- AGENTS.md — hard rules 27, 44, 45, 50, 51, 52-55.