Skip to main content

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:

Implementation detail

Source-level examples and database statements are maintained in the module repositories. This public page describes the operational contract and configuration intent.

Token Security

PropertyValue
EncryptionAES-256-GCM (authenticated encryption, 128-bit tag)
Key derivationPBKDF2 / RFC2898, 600 000 iterations, SHA-256, 256-bit key
Key sourceCoreApiKey from configuration
EncodingBase64Url (URL-safe) of version(1) + salt(16) + nonce(12) + ciphertext + tag(16)
TTL2 minutes at the magic login endpoint (the shared MagicTokenService.DefaultTtl is 10 min, but the login flow overrides it to 120 s)
Single-useToken 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

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
GET /auth/v1/magic?token=<base64url_encrypted_token>

When the user clicks this link, AuthProxy:

  1. Decodes the Base64Url token
  2. Decrypts using AES-256 with the CoreApiKey
  3. Validates the token hasn't expired (2-minute TTL)
  4. Matches the challenge ID to a pending authentication challenge
  5. Creates a session and sets the session cookie
  6. Redirects the user to the application

Purposes

The MagicToken model carries a Purpose field, reserved for three values:

PurposeUse Case
loginPasswordless authentication
confirmEmail address verification (reserved)
resetPassword reset flow (reserved)

Currently AuthProxy only ever generates tokens with Purpose = "login"; confirm / reset are 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; the login_type column stores the sequential LoginType enum (Email = 7), not the LoginMethod bitmask
  • Rate limiting applies to the login_email endpoint