Magic-Link — Authentication Flows
1. Magic link only (Free)
Simplest flow. User enters email, receives a link, clicks it, is logged in.
User Frontend Strapi + Magic-Link Mailbox
│ │ │ │
│──email──▶ │ │ │
│ │──POST /api/magic-link/send─▶ │
│ │ │──email with link──▶│
│ │ │ │
│◀──────── opens email ──────────────────────────────────────────────────│
│────clicks link─▶ │ │ │
│ │──GET /api/magic-link/verify?token=...──▶ │
│ │◀──────────────── JWT ───────────────────── │
│ │ │ │
│◀── logged in ───────────│ │ │API calls:
bash
POST /api/magic-link/send
{ "email": "user@example.com" }
→ 200 { "message": "Magic link sent" }
GET /api/magic-link/verify?token=...
→ 200 { "jwt": "...", "user": { "id": 1, "email": "user@example.com" } }2. Link + Email OTP (Premium)
After clicking the link, the user enters a 6-digit code sent to their email. Defeats email-preview scanners and URL forwarding.
User clicks link
↓
Magic-Link renders OTP form (not a JWT yet)
↓
Second email with 6-digit OTP sent
↓
User enters OTP → POST /api/magic-link/otp/verify → JWT issuedbash
POST /api/magic-link/otp/verify
{ "otp": "123456", "magicLinkId": "..." }
→ 200 { "jwt": "...", "user": {...} }3. Link + TOTP (Advanced)
Authenticator app required. Highest usability-security balance.
User clicks link → renders TOTP form
↓
User opens authenticator app → reads 6-digit code
↓
Submits code → POST /api/magic-link/totp/verify → JWTPrerequisite: user must have enrolled TOTP once (onboarding flow).
bash
# Enrollment (one-time, during onboarding)
POST /api/magic-link/totp/enroll
→ 200 { "qrCode": "otpauth://...", "secret": "..." }
# Verification (every login)
POST /api/magic-link/totp/verify
{ "code": "123456" }
→ 200 { "jwt": "...", "user": {...} }4. TOTP-only (Advanced)
Skip the email roundtrip entirely. Useful for internal tools where users already have authenticator apps.
User enters email + username + TOTP code
↓
POST /api/magic-link/totp-only
→ JWT (no email sent)5. Password + MFA (Advanced)
Traditional password login with TOTP second factor. For orgs that require password auth for compliance.
POST /api/auth/local { identifier, password }
→ 200 { magicLinkId, requiresTotp: true }
↓
POST /api/magic-link/totp/verify { magicLinkId, code }
→ 200 { jwt, user }Flow selection recommendations
| Scenario | Recommended flow |
|---|---|
| Consumer app, low-stakes | 1. Magic link only |
| Consumer app, high-stakes (e-commerce) | 2. Link + Email OTP |
| B2B SaaS dashboard | 3. Link + TOTP |
| Internal tools, ops dashboards | 4. TOTP-only |
| Regulated industry (finance, health) | 5. Password + MFA |
Edge cases
Expired token
bash
GET /api/magic-link/verify?token=expired
→ 410 Gone { "error": "TOKEN_EXPIRED" }Token reuse attempt
bash
GET /api/magic-link/verify?token=already-used
→ 410 Gone { "error": "TOKEN_ALREADY_USED" }Rate limit hit
bash
POST /api/magic-link/send (11th time in 1 hour)
→ 429 Too Many Requests { "error": "RATE_LIMITED", "retryAfter": 2400 }User account disabled
bash
GET /api/magic-link/verify?token=...
→ 403 Forbidden { "error": "ACCOUNT_DISABLED" }Next: MFA & TOTP →