Implementing Magic Link Authentication

Implementing Magic Link Authentication

Magic links provide a secure, user-friendly alternative to passwords. This tutorial shows you how to implement magic link authentication in Keycloak, handle edge cases, and create a seamless user experience.

What Are Magic Links?

Magic links are:

  • Time-limited, single-use URLs sent to users’ email
  • Click to authenticate without entering a password
  • Perfect for reducing friction in sign-up/login flows
  • More secure than weak passwords

How It Works in Keycloak

Keycloak provides action tokens that we can leverage for magic links:

  1. User requests magic link
  2. Generate custom action token
  3. Send token via email
  4. Validate token on click
  5. Create user session

Step 1: Create Custom Authenticator

Magic Link Authenticator SPI

public class MagicLinkAuthenticator implements Authenticator {
    
    private static final Logger logger = Logger.getLogger(MagicLinkAuthenticator.class);
    
    @Override
    public void authenticate(AuthenticationFlowContext context) {
        // Show email input form
        Response response = context.form()
            .setAttribute("realm", context.getRealm())
            .createForm("magic-link-email.ftl");
        
        context.challenge(response);
    }
    
    @Override
    public void action(AuthenticationFlowContext context) {
        MultivaluedMap<String, String> formData = 
            context.getHttpRequest().getDecodedFormParameters();
        
        String email = formData.getFirst("email");
        
        if (email == null || email.trim().isEmpty()) {
            context.failureChallenge(AuthenticationFlowError.INVALID_USER,
                context.form()
                    .setError("Email is required")
                    .createForm("magic-link-email.ftl"));
            return;
        }
        
        // Check if user exists
        UserModel user = KeycloakModelUtils.findUserByNameOrEmail(
            context.getSession(), context.getRealm(), email);
        
        if (user == null) {
            // Optional: Create user if doesn't exist
            if (isRegistrationAllowed(context)) {
                user = createUser(context, email);
            } else {
                context.failureChallenge(AuthenticationFlowError.INVALID_USER,
                    context.form()
                        .setError("User not found")
                        .createForm("magic-link-email.ftl"));
                return;
            }
        }
        
        // Generate and send magic link
        String token = generateMagicLinkToken(context, user);
        sendMagicLink(context, user, token);
        
        // Show success page
        Response response = context.form()
            .setAttribute("email", email)
            .createForm("magic-link-sent.ftl");
        
        context.challenge(response);
    }
    
    private String generateMagicLinkToken(AuthenticationFlowContext context, UserModel user) {
        int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
        
        // Create custom action token
        MagicLinkActionToken token = new MagicLinkActionToken(
            user.getId(),
            absoluteExpirationInSecs,
            context.getAuthenticationSession().getParentSession().getId()
        );
        
        // Sign token
        String encodedToken = token.serialize(
            context.getSession(),
            context.getRealm(),
            context.getUriInfo()
        );
        
        return encodedToken;
    }
    
    private void sendMagicLink(AuthenticationFlowContext context, 
                              UserModel user, String token) {
        String link = context.getUriInfo().getBaseUriBuilder()
            .path("realms")
            .path(context.getRealm().getName())
            .path("login-actions")
            .path("action-token")
            .queryParam("key", token)
            .queryParam("client_id", context.getAuthenticationSession().getClient().getClientId())
            .build()
            .toString();
        
        // Send email
        try {
            context.getSession().getProvider(EmailTemplateProvider.class)
                .setRealm(context.getRealm())
                .setUser(user)
                .send("magicLinkEmail", 
                      "Login to " + context.getRealm().getDisplayName(),
                      Collections.singletonMap("link", link));
        } catch (EmailException e) {
            logger.error("Failed to send magic link email", e);
            context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR,
                context.form()
                    .setError("Failed to send email")
                    .createForm("magic-link-email.ftl"));
        }
    }
}

Magic Link Action Token

@JsonTypeName("magic-link")
public class MagicLinkActionToken extends DefaultActionToken {
    
    public static final String TOKEN_TYPE = "magic-link";
    
    @JsonProperty("sid")
    private String sessionId;
    
    public MagicLinkActionToken(String userId, int absoluteExpirationInSecs, String sessionId) {
        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);
        this.sessionId = sessionId;
    }
    
    // Required for deserialization
    private MagicLinkActionToken() {
    }
    
    public String getSessionId() {
        return sessionId;
    }
    
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}

Action Token Handler

public class MagicLinkActionTokenHandler extends AbstractActionTokenHandler<MagicLinkActionToken> {
    
    @Override
    public Response handleToken(MagicLinkActionToken token, ActionTokenContext<MagicLinkActionToken> tokenContext) {
        AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(tokenContext.getSession());
        AuthenticationSessionModel authSession = authSessionManager.getAuthenticationSessionByIdAndClient(
            tokenContext.getRealm(), token.getSessionId(), tokenContext.getClientSession().getClient());
        
        if (authSession == null) {
            return tokenContext.getSession().getProvider(LoginFormsProvider.class)
                .setError("Invalid or expired link")
                .createErrorPage(Response.Status.BAD_REQUEST);
        }
        
        UserModel user = tokenContext.getSession().users()
            .getUserById(tokenContext.getRealm(), token.getUserId());
        
        if (user == null || !user.isEnabled()) {
            return tokenContext.getSession().getProvider(LoginFormsProvider.class)
                .setError("User not found or disabled")
                .createErrorPage(Response.Status.BAD_REQUEST);
        }
        
        // Set user as authenticated
        authSession.setAuthenticatedUser(user);
        
        // Complete authentication
        return tokenContext.getSession().getProvider(LoginFormsProvider.class)
            .setAuthenticationSession(authSession)
            .setSuccess("Login successful")
            .createLoginSuccessPage();
    }
    
    @Override
    public String getDisplayType() {
        return "Magic Link";
    }
    
    @Override
    public String getHelpText() {
        return "Handles magic link authentication";
    }
}

Step 2: Create Email Templates

Email Template (HTML)

<#import "template.ftl" as layout>
<@layout.emailLayout>
    <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
        <tr>
            <td style="padding: 40px 20px;">
                <h1 style="margin: 0 0 20px 0; font-size: 24px; line-height: 30px; color: #333333; font-weight: bold;">
                    Your Magic Link is Here! ✨
                </h1>
                
                <p style="margin: 0 0 20px 0; font-size: 16px; line-height: 25px; color: #666666;">
                    Click the button below to securely log in to ${realmName}. 
                    This link will expire in ${linkExpiration} minutes.
                </p>
                
                <!-- Button -->
                <table role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: 30px auto;">
                    <tr>
                        <td style="border-radius: 8px; background: #007bff;">
                            <a href="${link}" target="_blank" 
                               style="background: #007bff; border: 15px solid #007bff; font-family: sans-serif; font-size: 16px; line-height: 110%; text-align: center; text-decoration: none; display: block; border-radius: 8px; font-weight: bold;">
                                <span style="color:#ffffff;">Log In Now</span>
                            </a>
                        </td>
                    </tr>
                </table>
                
                <p style="margin: 20px 0; font-size: 14px; line-height: 20px; color: #999999;">
                    Or copy and paste this link in your browser:
                </p>
                
                <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 20px; color: #666666; word-break: break-all;">
                    ${link}
                </p>
                
                <!-- Security notice -->
                <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-top: 40px;">
                    <tr>
                        <td style="padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
                            <p style="margin: 0; font-size: 14px; color: #666666;">
                                <strong>🔒 Security Notice:</strong><br>
                                • This link can only be used once<br>
                                • It will expire in ${linkExpiration} minutes<br>
                                • If you didn't request this, please ignore this email
                            </p>
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</@layout.emailLayout>

Email Template (Text)

Your Magic Link for ${realmName}

Click the link below to log in:
${link}

This link will expire in ${linkExpiration} minutes and can only be used once.

If you didn't request this login link, please ignore this email.

Security tips:
- Never share this link with anyone
- Make sure you're on the correct website
- The link expires after one use

Step 3: Frontend Implementation

React Component

import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

const MagicLinkLogin = () => {
  const [email, setEmail] = useState('');
  const [status, setStatus] = useState('idle'); // idle, sending, sent, error
  const [searchParams] = useSearchParams();
  
  useEffect(() => {
    // Check if returning from email click
    const token = searchParams.get('token');
    const error = searchParams.get('error');
    
    if (token) {
      handleMagicLinkCallback(token);
    } else if (error) {
      setStatus('error');
    }
  }, [searchParams]);
  
  const requestMagicLink = async (e) => {
    e.preventDefault();
    setStatus('sending');
    
    try {
      const response = await fetch('/auth/magic-link', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email })
      });
      
      if (response.ok) {
        setStatus('sent');
      } else {
        const error = await response.json();
        throw new Error(error.message || 'Failed to send magic link');
      }
    } catch (error) {
      console.error('Magic link error:', error);
      setStatus('error');
    }
  };
  
  const handleMagicLinkCallback = async (token) => {
    try {
      const response = await fetch('/auth/verify-magic-link', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token })
      });
      
      if (response.ok) {
        const { redirectUrl } = await response.json();
        window.location.href = redirectUrl || '/dashboard';
      } else {
        setStatus('error');
      }
    } catch (error) {
      console.error('Verification error:', error);
      setStatus('error');
    }
  };
  
  return (
    <div className="magic-link-container">
      {status === 'idle' || status === 'error' ? (
        <form onSubmit={requestMagicLink} className="magic-link-form">
          <h2>Sign in with Magic Link</h2>
          <p>We'll send you a secure link to sign in instantly.</p>
          
          {status === 'error' && (
            <div className="alert alert-error">
              Something went wrong. Please try again.
            </div>
          )}
          
          <div className="form-group">
            <label htmlFor="email">Email Address</label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="[email protected]"
              required
              autoFocus
              className="form-control"
            />
          </div>
          
          <button 
            type="submit" 
            disabled={status === 'sending'}
            className="btn btn-primary"
          >
            {status === 'sending' ? 'Sending...' : 'Send Magic Link'}
          </button>
          
          <p className="alternative-login">
            Or <a href="/login">sign in with password</a>
          </p>
        </form>
      ) : status === 'sent' ? (
        <div className="magic-link-sent">
          <div className="success-icon">📧</div>
          <h2>Check Your Email!</h2>
          <p>We sent a magic link to <strong>{email}</strong></p>
          <p>Click the link in the email to sign in.</p>
          
          <div className="email-tips">
            <h3>Didn't receive it?</h3>
            <ul>
              <li>Check your spam folder</li>
              <li>Make sure you entered the right email</li>
              <li>
                <button 
                  onClick={() => setStatus('idle')}
                  className="btn-link"
                >
                  Try again with a different email
                </button>
              </li>
            </ul>
          </div>
        </div>
      ) : null}
    </div>
  );
};

// Styles
const styles = `
.magic-link-container {
  max-width: 400px;
  margin: 50px auto;
  padding: 40px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.magic-link-form h2 {
  margin: 0 0 10px 0;
  font-size: 24px;
  font-weight: 600;
}

.magic-link-sent {
  text-align: center;
}

.success-icon {
  font-size: 64px;
  margin-bottom: 20px;
}

.email-tips {
  margin-top: 40px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
  text-align: left;
}

.email-tips h3 {
  margin: 0 0 10px 0;
  font-size: 16px;
}

.email-tips ul {
  margin: 0;
  padding-left: 20px;
}

.btn-link {
  background: none;
  border: none;
  color: #007bff;
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
}
`;

Step 4: Backend API Implementation

Node.js/Express Backend

const crypto = require('crypto');
const jwt = require('jsonwebtoken');

// Magic link configuration
const MAGIC_LINK_EXPIRY = 15 * 60; // 15 minutes
const MAGIC_LINK_SECRET = process.env.MAGIC_LINK_SECRET;

// Store used tokens (use Redis in production)
const usedTokens = new Set();

// Request magic link endpoint
app.post('/auth/magic-link', async (req, res) => {
  try {
    const { email } = req.body;
    
    if (!email || !isValidEmail(email)) {
      return res.status(400).json({ 
        error: 'Valid email required' 
      });
    }
    
    // Rate limiting check
    if (await isRateLimited(email)) {
      return res.status(429).json({ 
        error: 'Too many requests. Please try again later.' 
      });
    }
    
    // Generate secure token
    const token = crypto.randomBytes(32).toString('hex');
    const payload = {
      email,
      token,
      type: 'magic-link',
      exp: Math.floor(Date.now() / 1000) + MAGIC_LINK_EXPIRY
    };
    
    const signedToken = jwt.sign(payload, MAGIC_LINK_SECRET);
    
    // Store token hash for validation
    await storeToken(email, crypto.createHash('sha256').update(token).digest('hex'));
    
    // Generate link
    const magicLink = `${process.env.APP_URL}/auth/magic-link?token=${signedToken}`;
    
    // Send email
    await sendMagicLinkEmail(email, magicLink, MAGIC_LINK_EXPIRY / 60);
    
    // Log for security monitoring
    logger.info('Magic link requested', { 
      email, 
      ip: req.ip,
      userAgent: req.get('user-agent')
    });
    
    res.json({ 
      message: 'Magic link sent',
      expiresIn: MAGIC_LINK_EXPIRY 
    });
    
  } catch (error) {
    logger.error('Magic link error', error);
    res.status(500).json({ 
      error: 'Failed to send magic link' 
    });
  }
});

// Verify magic link endpoint
app.post('/auth/verify-magic-link', async (req, res) => {
  try {
    const { token } = req.body;
    
    if (!token) {
      return res.status(400).json({ 
        error: 'Token required' 
      });
    }
    
    // Verify JWT
    let payload;
    try {
      payload = jwt.verify(token, MAGIC_LINK_SECRET);
    } catch (error) {
      return res.status(401).json({ 
        error: 'Invalid or expired token' 
      });
    }
    
    // Check if token was already used
    const tokenHash = crypto.createHash('sha256').update(payload.token).digest('hex');
    if (usedTokens.has(tokenHash)) {
      return res.status(401).json({ 
        error: 'Token already used' 
      });
    }
    
    // Validate stored token
    const storedHash = await getStoredToken(payload.email);
    if (storedHash !== tokenHash) {
      return res.status(401).json({ 
        error: 'Invalid token' 
      });
    }
    
    // Mark token as used
    usedTokens.add(tokenHash);
    await deleteStoredToken(payload.email);
    
    // Get or create user in Keycloak
    const keycloakToken = await authenticateUserWithKeycloak(payload.email);
    
    // Create session
    req.session.accessToken = keycloakToken.access_token;
    req.session.refreshToken = keycloakToken.refresh_token;
    req.session.email = payload.email;
    
    // Log successful login
    logger.info('Magic link login successful', { 
      email: payload.email,
      ip: req.ip
    });
    
    res.json({ 
      success: true,
      redirectUrl: '/dashboard'
    });
    
  } catch (error) {
    logger.error('Magic link verification error', error);
    res.status(500).json({ 
      error: 'Verification failed' 
    });
  }
});

// Helper functions
async function authenticateUserWithKeycloak(email) {
  // Exchange magic link for Keycloak tokens
  const response = await fetch(`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'password', // Use service account
      client_id: ADMIN_CLIENT_ID,
      client_secret: ADMIN_CLIENT_SECRET,
      username: 'service-account',
      password: SERVICE_ACCOUNT_PASSWORD
    })
  });
  
  const adminToken = await response.json();
  
  // Create user session programmatically
  // This is a simplified version - implement according to your needs
  return createUserSession(email, adminToken.access_token);
}

// Email sending
async function sendMagicLinkEmail(email, link, expiryMinutes) {
  const mailOptions = {
    from: '"Your App" <[email protected]>',
    to: email,
    subject: '🔐 Your Magic Sign-In Link',
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>Sign in to Your App</h2>
        <p>Click the button below to sign in instantly:</p>
        <div style="text-align: center; margin: 30px 0;">
          <a href="${link}" 
             style="background-color: #007bff; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
            Sign In Now
          </a>
        </div>
        <p style="color: #666; font-size: 14px;">
          This link expires in ${expiryMinutes} minutes and can only be used once.
        </p>
        <p style="color: #666; font-size: 14px;">
          If you didn't request this, please ignore this email.
        </p>
      </div>
    `,
    text: `Sign in to Your App\n\nClick here to sign in: ${link}\n\nThis link expires in ${expiryMinutes} minutes.`
  };
  
  await transporter.sendMail(mailOptions);
}

// Rate limiting
const loginAttempts = new Map();

async function isRateLimited(email) {
  const key = `magic-link:${email}`;
  const attempts = loginAttempts.get(key) || [];
  const now = Date.now();
  
  // Remove old attempts (older than 1 hour)
  const recentAttempts = attempts.filter(time => now - time < 3600000);
  
  // Allow max 3 attempts per hour
  if (recentAttempts.length >= 3) {
    return true;
  }
  
  recentAttempts.push(now);
  loginAttempts.set(key, recentAttempts);
  
  return false;
}

Step 5: Security Enhancements

Token Security

// Secure token generation
function generateSecureToken() {
  // Use crypto.randomBytes for cryptographically secure tokens
  const token = crypto.randomBytes(32).toString('base64url');
  
  // Add timestamp to prevent replay attacks
  const timestamp = Date.now();
  const payload = `${token}.${timestamp}`;
  
  // Sign the payload
  const signature = crypto
    .createHmac('sha256', process.env.TOKEN_SECRET)
    .update(payload)
    .digest('base64url');
  
  return `${payload}.${signature}`;
}

// Token validation with timing attack prevention
function validateSecureToken(fullToken, maxAgeMs = 900000) { // 15 minutes
  const parts = fullToken.split('.');
  if (parts.length !== 3) return false;
  
  const [token, timestamp, providedSignature] = parts;
  const payload = `${token}.${timestamp}`;
  
  // Verify signature (constant-time comparison)
  const expectedSignature = crypto
    .createHmac('sha256', process.env.TOKEN_SECRET)
    .update(payload)
    .digest('base64url');
  
  if (!crypto.timingSafeEqual(
    Buffer.from(providedSignature),
    Buffer.from(expectedSignature)
  )) {
    return false;
  }
  
  // Check age
  const age = Date.now() - parseInt(timestamp);
  if (age > maxAgeMs) return false;
  
  return true;
}

Prevent Email Enumeration

// Always return same response regardless of email existence
app.post('/auth/magic-link', async (req, res) => {
  const { email } = req.body;
  
  // Always return success
  res.json({ 
    message: 'If an account exists with this email, a magic link has been sent.' 
  });
  
  // Process async to prevent timing attacks
  setImmediate(async () => {
    try {
      const user = await findUserByEmail(email);
      if (user) {
        await sendMagicLink(email);
      }
    } catch (error) {
      logger.error('Magic link processing error', error);
    }
  });
});

Device Fingerprinting

// Add device fingerprinting for additional security
function generateDeviceFingerprint(req) {
  const components = [
    req.get('user-agent'),
    req.get('accept-language'),
    req.get('accept-encoding'),
    req.ip
  ];
  
  return crypto
    .createHash('sha256')
    .update(components.join('|'))
    .digest('hex');
}

// Store and validate device fingerprints
app.post('/auth/verify-magic-link', async (req, res) => {
  const currentFingerprint = generateDeviceFingerprint(req);
  const storedFingerprint = await getStoredFingerprint(payload.token);
  
  if (currentFingerprint !== storedFingerprint) {
    // New device - might require additional verification
    logger.warn('Magic link used from different device', {
      email: payload.email,
      originalDevice: storedFingerprint,
      currentDevice: currentFingerprint
    });
    
    // Optional: Require additional verification
    if (STRICT_DEVICE_CHECK) {
      return res.status(401).json({
        error: 'Security check failed. Please request a new link.'
      });
    }
  }
});

Step 6: Advanced Features

Remember Me Option

// Extended session for trusted devices
app.post('/auth/magic-link', async (req, res) => {
  const { email, rememberMe } = req.body;
  
  const tokenExpiry = rememberMe ? 
    30 * 24 * 60 * 60 : // 30 days
    15 * 60; // 15 minutes
  
  // Include in token payload
  const payload = {
    email,
    rememberMe,
    exp: Math.floor(Date.now() / 1000) + tokenExpiry
  };
});

Multi-Factor Authentication

// Require additional factor after magic link
app.post('/auth/verify-magic-link', async (req, res) => {
  // ... verify magic link ...
  
  const user = await getUser(payload.email);
  
  if (user.mfaEnabled) {
    // Don't complete login yet
    req.session.pendingMFA = {
      userId: user.id,
      email: user.email
    };
    
    return res.json({
      requiresMFA: true,
      mfaType: user.mfaType,
      redirectUrl: '/auth/mfa'
    });
  }
  
  // Complete login for non-MFA users
  completeLogin(user, req, res);
});

Analytics and Monitoring

// Track magic link metrics
const metrics = {
  requested: new promClient.Counter({
    name: 'magic_links_requested_total',
    help: 'Total magic links requested',
    labelNames: ['status']
  }),
  
  verified: new promClient.Counter({
    name: 'magic_links_verified_total',
    help: 'Total magic links verified',
    labelNames: ['status', 'device_match']
  }),
  
  latency: new promClient.Histogram({
    name: 'magic_link_verification_duration_seconds',
    help: 'Time to verify magic link',
    buckets: [0.1, 0.5, 1, 2, 5]
  })
};

// Track in endpoints
metrics.requested.inc({ status: 'success' });
metrics.verified.inc({ 
  status: 'success', 
  device_match: fingerprint === stored ? 'true' : 'false' 
});

Best Practices

1. Email Delivery

  • Use transactional email service (SendGrid, AWS SES)
  • Set proper SPF, DKIM, DMARC records
  • Monitor delivery rates
  • Provide clear sender name

2. User Experience

  • Show clear success/error messages
  • Provide alternative login methods
  • Handle email delivery delays gracefully
  • Support “Resend” functionality

3. Security

  • Single-use tokens only
  • Short expiration times (10-15 minutes)
  • Rate limit requests per email
  • Log all authentication attempts
  • Implement device verification for sensitive apps

4. Testing

describe('Magic Link Authentication', () => {
  it('should send magic link for valid email', async () => {
    const response = await request(app)
      .post('/auth/magic-link')
      .send({ email: '[email protected]' });
    
    expect(response.status).toBe(200);
    expect(mockEmailService.send).toHaveBeenCalledWith(
      expect.objectContaining({
        to: '[email protected]',
        subject: expect.stringContaining('Magic')
      })
    );
  });
  
  it('should reject used tokens', async () => {
    const token = generateTestToken();
    
    // First use - should succeed
    await request(app)
      .post('/auth/verify-magic-link')
      .send({ token })
      .expect(200);
    
    // Second use - should fail
    await request(app)
      .post('/auth/verify-magic-link')
      .send({ token })
      .expect(401);
  });
});

Common Issues and Solutions

Email Delivery Problems

// Implement retry logic
async function sendMagicLinkWithRetry(email, link, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await sendEmail(email, link);
      return;
    } catch (error) {
      logger.warn(`Email send attempt ${i + 1} failed`, error);
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
}

Token Expiration Handling

// Graceful expiration handling
app.post('/auth/verify-magic-link', async (req, res) => {
  try {
    // ... verify token ...
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({
        error: 'Link expired',
        code: 'LINK_EXPIRED',
        suggestion: 'Request a new magic link'
      });
    }
    throw error;
  }
});

Next Steps