Passwordless authentication with email 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.
Configure the email provider for magic link authentication:
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!,
},
});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.comFor Gmail SMTP, you need to set up an App Password:
SMTP_PASSFor production, consider using dedicated email services:
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-sendgrid-api-keySMTP_HOST=smtp.mailgun.org
SMTP_PORT=587
SMTP_USER=your-mailgun-username
SMTP_PASS=your-mailgun-passwordSMTP_HOST=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
SMTP_USER=your-ses-username
SMTP_PASS=your-ses-passwordHere's how to implement magic link authentication:
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>
);
}Create an API endpoint to handle magic link requests:
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 }
);
}
}Customize the magic link email template:
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 links include several security features:
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
},
});Test your magic link implementation:
npm run devSolutions to common magic link problems:
Error: "Authentication failed"
Solution: Check your SMTP credentials and ensure 2FA is enabled for Gmail.
Error: Email not delivered
Solution: Check spam folder, verify SMTP settings, and test with a different email provider.
Error: "Link has expired"
Solution: Request a new magic link or increase the expiration time.
For production deployment:
Now that you have magic links working, you can: