Email-based magic link authentication allowing users to sign in without a password.
The Passwordless feature implements magic link authentication - users receive a secure, time-limited link via email that authenticates them when clicked. This provides an alternative to password-based authentication with improved security (no passwords to leak) and better UX (no passwords to remember).
Key capabilities:
- Sign in existing users via email
- Auto-create new users on first sign-in (configurable)
- Same-browser verification for enhanced security (optional)
- Async email delivery via Vapor Queues
Passage.Configuration(
// ... other config ...
passwordless: .init(
revokeExistingTokens: true, // Revoke old tokens on new login
emailMagicLink: .email(
routes: .email, // Default route paths
useQueues: true, // Send emails async via Vapor Queues
linkExpiration: 15 * 60, // 15 minutes
maxAttempts: 5, // Max failed verification attempts
autoCreateUser: true, // Create user if doesn't exist
requireSameBrowser: false // Require verification in same browser
)
)
)| Option | Type | Default | Description |
|---|---|---|---|
revokeExistingTokens |
Bool |
true |
Revoke existing refresh tokens when issuing new ones |
emailMagicLink |
MagicLink? |
.email() |
Email magic link config (nil to disable) |
emailMagicLink.useQueues |
Bool |
true |
Send emails via Vapor Queues (async) |
emailMagicLink.linkExpiration |
TimeInterval |
900 (15 min) |
Magic link validity duration |
emailMagicLink.maxAttempts |
Int |
5 |
Max failed verification attempts before invalidation |
emailMagicLink.autoCreateUser |
Bool |
true |
Create new user if email not found |
emailMagicLink.requireSameBrowser |
Bool |
false |
Require verification from same browser session |
- User submits email address
- System finds existing user (or allows new user if
autoCreateUser: true) - Any existing magic links for this email are invalidated
- New magic link token generated and hashed
- Token stored with expiration time
- Email sent with verification link (sync or via queue)
- User clicks link in email
- Token extracted from URL query parameter
- Token hashed and looked up in store
- Validation checks: not expired, attempts within limit
- Same-browser check (if enabled)
- User found or created (if
autoCreateUser: true) - Email marked as verified
- Magic link invalidated (one-time use)
- JWT access + refresh tokens issued
When autoCreateUser: true, users who don't exist are created during verification:
- New user created with email identifier
- Email automatically marked as verified
- No password set (passwordless-only user)
When autoCreateUser: false, requesting a magic link for unknown email throws magicLinkEmailNotFound.
| Method | Default Path | Description |
|---|---|---|
| POST | /auth/magic-link/email |
Request magic link |
| GET | /auth/magic-link/email/verify |
Verify token (from email link) |
| POST | /auth/magic-link/email/resend |
Resend magic link |
Magic Link URL Format:
https://yourapp.com/auth/magic-link/email/verify?token=<opaque_token>
sequenceDiagram
participant User
participant App
participant Passage
participant Store
participant EmailQueue
User->>App: POST /magic-link/email (email)
App->>Passage: requestEmailMagicLink(email)
Passage->>Store: Find user by email
Store-->>Passage: User (or nil)
Passage->>Store: Invalidate old magic links
Passage->>Passage: Generate token + hash
Passage->>Store: Store magic link
Passage->>EmailQueue: Dispatch email job
Passage-->>App: 200 OK
App-->>User: Check your email
EmailQueue->>User: Email with magic link
User->>App: GET /verify?token=xxx
App->>Passage: verifyEmailMagicLink(token)
Passage->>Passage: Hash token
Passage->>Store: Find magic link by hash
Store-->>Passage: Magic link record
Passage->>Passage: Validate expiration & attempts
alt User exists
Passage->>Store: Mark email verified
else Auto-create enabled
Passage->>Store: Create user with email
end
Passage->>Store: Invalidate magic link
Passage->>Passage: Issue tokens
Passage-->>App: AuthUser (tokens)
App-->>User: Authenticated
Magic link tokens use a secure design:
- Opaque tokens - Random, unpredictable values
- Hashed storage - Only SHA256 hash stored in database, never plaintext
- Single use - Invalidated immediately after successful verification
- Time-limited - Configurable expiration (default 15 minutes)
- Attempt limiting - Tracks failed attempts to prevent brute force
When requireSameBrowser: true:
Request phase:
- Generate session token
- Store hash in database with magic link
- Store raw token in Vapor session (server-side)
Verification phase:
- Retrieve session token from Vapor session
- Hash and compare with stored hash
- Mismatch throws
magicLinkDifferentBrowser
This prevents leaked links from being used on different devices/browsers.
When useQueues: true, emails are sent via Vapor Queues:
- Non-blocking request handling
- Automatic retry (up to 3 attempts)
- Requires Vapor Queues configured in your app
When useQueues: false, emails are sent synchronously during the request.
For magic link emails, use passage-mailgun - a ready-to-use Mailgun implementation:
| Error | Trigger |
|---|---|
magicLinkInvalid |
Token not found or hash mismatch |
magicLinkExpired |
Link past expiration time |
magicLinkMaxAttempts |
Too many failed verification attempts |
magicLinkEmailNotFound |
Email not found and autoCreateUser: false |
magicLinkDifferentBrowser |
Verification from different browser (when required) |
emailDeliveryNotConfigured |
Email delivery service not set up |
emailMagicLinkNotConfigured |
Magic link feature disabled |
For HTML form-based flows, configure view templates:
views: .init(
magicLinkRequest: .init(path: "auth/magic-link-request"), // Request form
magicLinkVerify: .init(path: "auth/magic-link-verify") // Verification result
)The request view shows a form for entering email. The verify view shows success or error messages after clicking the link.
- Account - Password-based authentication (alternative)
- Tokens - JWT access and refresh token management
- Verification - Email verification codes (different from magic links)
- Views - HTML form rendering for web-based flows