Magic Links
Magic links provide a passwordless login experience via email. Instead of entering an OTP code, the user clicks a link in their email to authenticate instantly.
How It Works
1. User requests login with email (with the challengeId from login_options)
2. AuthProxy generates an AES-256-GCM encrypted token
3. Email with magic link is sent via Chat module
4. User clicks link → AuthProxy decrypts token → Session created
Flow
User AuthProxy Chat Module
│ │ │
│ login_email(email) │ │
│────────────────────────>│ │
│ │ SendEmailOTP(code+link) │
│ │───────────────────────────>│
│ │ │ Send email
│ │ │──────────>
│ │ │
│ Click magic link │ │
│────────────────────────>│ │
│ │ Decrypt & validate token │
│ Session cookie │ │
│<────────────────────────│ │
Token Structure
The magic link contains an encrypted MagicToken:
Source-level examples and database statements are maintained in the module repositories. This public page describes the operational contract and configuration intent.
Token Security
| Property | Value |
|---|---|
| Encryption | AES-256-GCM (authenticated encryption, 128-bit tag) |
| Key derivation | PBKDF2 / RFC2898, 600 000 iterations, SHA-256, 256-bit key |
| Key source | CoreApiKey from configuration |
| Encoding | Base64Url (URL-safe) of version(1) + salt(16) + nonce(12) + ciphertext + tag(16) |
| TTL | 2 minutes at the magic login endpoint (the shared MagicTokenService.DefaultTtl is 10 min, but the login flow overrides it to 120 s) |
| Single-use | Token is tied to a challenge ID — used once, then the challenge expires |
Each token uses a fresh random salt + nonce, so the same email address always produces a different encrypted token. Decryption also accepts a legacy v1 format (AES-CBC, PBKDF2 100 iterations) for in-flight tokens during a deployment transition.
API Endpoints
Request Magic Link
curl -X POST http://authproxy/auth/v1/login_email \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"challengeId": "17234567890001"
}'
challengeId is required — obtain it from login_options first; the magic token is bound to it. The optional code field is supplied on the second call to confirm a manually entered OTP. The email will contain both:
- A 6-digit OTP code for manual entry
- A magic link for one-click login
Magic Link URL
GET /auth/v1/magic?token=<base64url_encrypted_token>
When the user clicks this link, AuthProxy:
- Decodes the Base64Url token
- Decrypts using AES-256 with the CoreApiKey
- Validates the token hasn't expired (2-minute TTL)
- Matches the challenge ID to a pending authentication challenge
- Creates a session and sets the session cookie
- Redirects the user to the application
Purposes
The MagicToken model carries a Purpose field, reserved for three values:
| Purpose | Use Case |
|---|---|
login | Passwordless authentication |
confirm | Email address verification (reserved) |
reset | Password reset flow (reserved) |
Currently AuthProxy only ever generates tokens with
Purpose = "login";confirm/resetare defined in the token model but not yet issued by any endpoint.
Configuration
Magic links work out of the box with the standard AuthProxy configuration. Requirements:
- CoreApiKey must be set in
appsettings.json(used as encryption key) - Chat module must be available for email delivery
- Email provider must be configured in the Chat module (SMTP, SendGrid, etc.)
Security Considerations
- Tokens are encrypted, not just signed — the email content cannot reveal the token payload
- The 2-minute TTL limits the attack window for intercepted emails
- Each token is tied to a specific challenge — replaying an expired token fails
- Magic link authentication is logged in
login_log; thelogin_typecolumn stores the sequentialLoginTypeenum (Email = 7), not theLoginMethodbitmask - Rate limiting applies to the
login_emailendpoint