Skip to main content

Magic link (passwordless email) login

Company-scoped magic link sign-in complements OAuth. Users request a one-time link by email; clicking the link (or calling the consume API) yields Shellui JWTs using the same session delivery options as OAuth (OAUTH_TOKEN_DELIVERY).


Enablement​

LayerControl
DeploymentMAGIC_LINK_ENABLED=true (default). Set false to disable all magic-link endpoints globally.
CompanyCompany.enable_magic_link (default true for new companies). Toggle via Admin REST (below) or Django admin → Company.
Capabilities APIGET /api/v1/settings?company_id=… returns enable_magic_link and includes magic_link in methods when enabled.

OAuth providers remain independent — a company can use magic link only, OAuth only, or both.


API​

POST /api/v1/magic-link/request

{
"company_id": 1,
"email": "ada@acme.com",
"redirect_to": "https://app.example.com/login/callback",
"client_timezone": "Europe/Paris",
"client_device_id": "optional-device-id"
}
  • redirect_to must match the company OAuth redirect allowlist (same rules as OAuth login).
  • When magic link is disabled, the API returns 403 with error_code: magic_link_disabled.
  • When enabled, the API always returns 200 with a generic message (does not reveal whether the email exists).
  • Sends the sign-in email directly from static Django templates in the repo (apps/authapi/templates/authapi/magic_link/, EN and FR, HTML and plain text, locale from user preference with EN fallback). Not configurable in admin or Actions. Optionally emits identity.auth.magic_link.requested to webhook Action rules (see actions.md).

Rate limits: AUTH_RATE_LIMIT_MAGIC_LINK (default 10/min) per client IP, email+company, and company.

Verify (browser)​

GET /api/v1/magic-link/verify?token=…&company_id=…

Renders a confirmation page and does not consume the token (safe for email link scanners). Submit the form with POST to the same URL (include company_id in the query string) to finish sign-in, apply company join rules, and redirect to redirect_to with tokens (session code or fragment).

Consume (JSON)​

POST /api/v1/magic-link/verify

{
"token": "…",
"company_id": 1
}

Returns the same JWT payload as POST /api/v1/token / OAuth finalize on success; 403 when company access is pending or denied.


Security​

TopicBehavior
TTLMAGIC_LINK_TTL_SECONDS (default 1800 = 30 minutes)
One-time useToken invalidated on successful consume
Link URLBuilt from JWT_ISSUER (HTTPS required when DEBUG=false)
Secrets in webhooksWebhook payloads include request_id, email, expires_at — not the raw token or sign-in URL. The email contains the one-time link only.
Webhook user fieldsuser_id, language, and region are included only when the email matches a user who already has membership in that company.
PrivacyRequest endpoint does not enumerate valid emails

Admin REST (Shellui admin)​

Staff or company owner JWT with the usual company scope (company_id query/body or company_id claim in the access token). Same authorization as /api/v1/scim and /api/v1/oauth-clients. The shellui/admin SPA can call these endpoints; a settings UI may ship later — this API is the contract.

MethodPathNotes
GET/api/v1/auth-methodsCompany magic-link + OAuth flags: enable_magic_link, magic_link_effective, magic_link_globally_enabled, enable_oauth, oauth_providers, methods
PATCH, PUT/api/v1/auth-methodsBody { "enable_magic_link": true | false } — persists on the company; when false, magic-link request/verify return 403 (magic_link_disabled)

Example GET response:

{
"enable_magic_link": true,
"magic_link_effective": true,
"magic_link_globally_enabled": true,
"enable_oauth": true,
"oauth_providers": ["github"],
"methods": ["magic_link", "oauth"]
}

When MAGIC_LINK_ENABLED=false on the deployment, magic_link_globally_enabled and magic_link_effective are false even if the company flag stays true (company setting is preserved for when the kill switch is lifted).


Disabling for a company​

  1. Admin REST: PATCH /api/v1/auth-methods?company_id=… with { "enable_magic_link": false }, or Django admin → Companies → uncheck Enable magic link.
  2. Optionally remove magic_link Action rules.

New companies default to magic link enabled (enable_magic_link=true).