Email Magic Links

Passwordless authentication with email magic links.

What are Magic Links?

Magic links are passwordless authentication links sent via email. Users click the link to automatically sign in without entering a password. This provides a seamless, secure authentication experience.

How Magic Links Work

  1. User enters their email address
  2. System generates a secure, time-limited token
  3. Magic link is sent to the user's email
  4. User clicks the link in their email
  5. System validates the token and signs the user in
  6. User is redirected to the application

Email Provider Setup

Configure the email provider for magic link authentication:

Basic Email Provider Configuration

Minimal setup for email magic links

import { email } from '@warpy-auth-sdk/core';

const emailProvider = email({
  server: 'smtp.gmail.com:587',
  from: 'noreply@yourdomain.com',
  auth: {
    user: process.env.SMTP_USER!,
    pass: process.env.SMTP_PASS!,
  },
});

SMTP Configuration

Set up your SMTP server configuration:

# .env.local
AUTH_SECRET=your-secret-key-min-32-chars-long
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@yourdomain.com

Gmail Setup

For Gmail SMTP, you need to set up an App Password:

  1. Enable 2-factor authentication on your Gmail account
  2. Go to Google Account settings → Security
  3. Under "2-Step Verification", click "App passwords"
  4. Generate a password for "Mail"
  5. Use this password as your SMTP_PASS

Gmail Security

  • Never use your regular Gmail password
  • App passwords are more secure than regular passwords
  • You can revoke app passwords at any time

Alternative SMTP Providers

For production, consider using dedicated email services:

SendGrid

SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-sendgrid-api-key

Mailgun

SMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=your-mailgun-username
SMTP_PASS=your-mailgun-password

AWS SES

SMTP_HOST=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
SMTP_USER=your-ses-username
SMTP_PASS=your-ses-password

Magic Link Implementation

Here's how to implement magic link authentication:

Magic Link Sign-in Page

Frontend form for email input

'use client';

import { useState } from 'react';

export default function MagicLinkSignIn() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setMessage('');

    try {
      const response = await fetch('/api/auth/signin/email', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email }),
      });

      if (response.ok) {
        setMessage('Check your email for the magic link!');
      } else {
        setMessage('Failed to send magic link. Please try again.');
      }
    } catch (error) {
      setMessage('An error occurred. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
            Sign in with Magic Link
          </h2>
          <p className="mt-2 text-center text-sm text-gray-600">
            Enter your email address and we'll send you a magic link
          </p>
        </div>
        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
          <div>
            <label htmlFor="email" className="sr-only">
              Email address
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
              placeholder="Email address"
            />
          </div>
          <div>
            <button
              type="submit"
              disabled={loading}
              className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
            >
              {loading ? 'Sending...' : 'Send Magic Link'}
            </button>
          </div>
          {message && (
            <div className={`text-sm text-center ${message.includes('Check your email') ? 'text-green-600' : 'text-red-600'}`}>
              {message}
            </div>
          )}
        </form>
      </div>
    </div>
  );
}

Magic Link API Endpoint

Create an API endpoint to handle magic link requests:

app/api/auth/signin/email/route.ts

API endpoint for sending magic links

import { authenticate, email } from '@warpy-auth-sdk/core';
import { NextRequest } from 'next/server';

const emailProvider = email({
  server: `${process.env.SMTP_HOST}:${process.env.SMTP_PORT}`,
  from: process.env.SMTP_FROM!,
  auth: {
    user: process.env.SMTP_USER!,
    pass: process.env.SMTP_PASS!,
  },
});

export async function POST(request: NextRequest) {
  try {
    const { email: userEmail } = await request.json();

    if (!userEmail) {
      return Response.json(
        { error: 'Email is required' },
        { status: 400 }
      );
    }

    const result = await authenticate(
      {
        secret: process.env.AUTH_SECRET!,
        provider: emailProvider,
      },
      request,
      { email: userEmail }
    );

    if (result.error) {
      return Response.json(
        { error: result.error },
        { status: 400 }
      );
    }

    return Response.json({
      success: true,
      message: 'Magic link sent successfully'
    });
  } catch (error) {
    console.error('Magic link error:', error);
    return Response.json(
      { error: 'Failed to send magic link' },
      { status: 500 }
    );
  }
}

Magic Link Email Template

Customize the magic link email template:

Email Template Configuration

Custom email template for magic links

import { email } from '@warpy-auth-sdk/core';

const emailProvider = email({
  server: `${process.env.SMTP_HOST}:${process.env.SMTP_PORT}`,
  from: process.env.SMTP_FROM!,
  auth: {
    user: process.env.SMTP_USER!,
    pass: process.env.SMTP_PASS!,
  },
  // Custom email template
  emailTemplate: {
    subject: 'Your magic link for My App',
    html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>Welcome to My App!</h2>
        <p>Click the link below to sign in:</p>
        <a href="${magicLink}"
           style="display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px;">
          Sign In
        </a>
        <p>This link will expire in 15 minutes.</p>
        <p>If you didn't request this, please ignore this email.</p>
      </div>
    `,
    text: `
      Welcome to My App!

      Click the link below to sign in:
      ${magicLink}

      This link will expire in 15 minutes.
      If you didn't request this, please ignore this email.
    `,
  },
});

Magic Link Security

Magic links include several security features:

  • Time-limited: Links expire after 15 minutes by default
  • Single-use: Each link can only be used once
  • Cryptographically secure: Tokens are generated using secure random bytes
  • Domain validation: Links are tied to your domain

Magic Link Configuration

Customize magic link behavior:

const emailProvider = email({
  server: `${process.env.SMTP_HOST}:${process.env.SMTP_PORT}`,
  from: process.env.SMTP_FROM!,
  auth: {
    user: process.env.SMTP_USER!,
    pass: process.env.SMTP_PASS!,
  },
  // Magic link configuration
  magicLink: {
    expiresIn: '15m',        // Link expiration time
    maxAttempts: 3,          // Max attempts per email
    rateLimit: '5m',         // Rate limit between attempts
  },
});

Testing Magic Links

Test your magic link implementation:

  1. Start your development server: npm run dev
  2. Visit your magic link sign-in page
  3. Enter a valid email address
  4. Check your email for the magic link
  5. Click the link to complete authentication
  6. Verify you're signed in and redirected correctly

Common Issues

Solutions to common magic link problems:

SMTP Authentication Failed

Error: "Authentication failed"
Solution: Check your SMTP credentials and ensure 2FA is enabled for Gmail.

Magic Link Not Received

Error: Email not delivered
Solution: Check spam folder, verify SMTP settings, and test with a different email provider.

Link Expired

Error: "Link has expired"
Solution: Request a new magic link or increase the expiration time.

Production Considerations

For production deployment:

  • Use a dedicated email service (SendGrid, Mailgun, etc.)
  • Set up proper DNS records (SPF, DKIM, DMARC)
  • Monitor email delivery rates
  • Implement rate limiting to prevent abuse
  • Set up email templates that match your brand

Magic Links Complete

You've successfully implemented passwordless authentication with magic links. Users can now sign in without passwords using just their email.

Next Steps

Now that you have magic links working, you can:

Email Magic Links | @warpy-auth-sdk/core