Complete the passwordless prerequisites and choose magic links only after testing email delivery, expiry, replay, and recovery behavior.
Magic Link Authentication
Magic links provide passwordless authentication by sending one-time login links via email. This is simpler to implement than passkeys and works on any device with email access.
Requesting a Magic Link
async function requestMagicLink(email: string, orgSlug?: string) {
try {
const response = await sso.magicLinks.request({
email,
orgSlug // Optional: organization context
});
console.log(response.message); // "Magic link sent to your email"
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 429) {
// Rate limit exceeded
console.error('Too many requests. Please wait before trying again.');
} else {
console.error('Failed to send magic link:', error.message);
}
}
return false;
}
}
Verifying a Magic Link
When users click the magic link in their email, your application needs to verify the token and complete authentication:
async function verifyMagicLink() {
// Extract token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
console.error('No token provided');
window.location.href = '/login';
return;
}
try {
// Verify the token
const auth = await sso.magicLinks.verify(token);
// Store tokens
localStorage.setItem('sso_access_token', auth.access_token);
localStorage.setItem('sso_refresh_token', auth.refresh_token);
sso.setAuthToken(auth.access_token);
// Redirect to application
window.location.href = '/dashboard';
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 404 || error.statusCode === 410) {
console.error('Magic link has expired or been used');
} else {
console.error('Verification failed:', error.message);
}
}
// Redirect back to login on error
window.location.href = '/login?error=invalid_link';
}
}
Complete React Magic Link Implementation
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL!
});
export function MagicLinkLogin() {
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
await sso.magicLinks.request({
email,
orgSlug: 'acme-corp' // Optional organization context
});
setSubmitted(true);
} catch (err) {
setIsLoading(false);
if (err instanceof SsoApiError) {
if (err.statusCode === 429) {
setError('Too many requests. Please wait a moment and try again.');
} else {
setError('Failed to send magic link. Please try again.');
}
}
}
};
if (submitted) {
return (
<div className="magic-link-sent">
<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. The link expires in 15 minutes.</p>
<button onClick={() => setSubmitted(false)}>
Use a different email
</button>
</div>
);
}
return (
<div className="magic-link-login">
<h2>Sign in with Magic Link</h2>
<p>Enter your email address and we'll send you a link to sign in</p>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email Address</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
autoFocus
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
{error && <div className="alert alert-error">{error}</div>}
<div className="alternative-methods">
<a href="/login/password">Sign in with password instead</a>
</div>
</div>
);
}
export function MagicLinkVerify() {
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying');
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
verifyToken();
}, []);
const verifyToken = async () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
setStatus('error');
setErrorMessage('No verification token found');
return;
}
try {
const auth = await sso.magicLinks.verify(token);
// Store tokens
localStorage.setItem('sso_access_token', auth.access_token);
localStorage.setItem('sso_refresh_token', auth.refresh_token);
sso.setAuthToken(auth.access_token);
setStatus('success');
// Redirect after brief success message
setTimeout(() => {
window.location.href = '/dashboard';
}, 1500);
} catch (error) {
setStatus('error');
if (error instanceof SsoApiError) {
if (error.statusCode === 404) {
setErrorMessage('This magic link has expired or been used');
} else if (error.statusCode === 410) {
setErrorMessage('This magic link has already been used');
} else {
setErrorMessage('Verification failed. Please request a new link.');
}
} else {
setErrorMessage('An unexpected error occurred');
}
}
};
if (status === 'verifying') {
return (
<div className="verifying">
<h2>Verifying your magic link...</h2>
<div className="spinner"></div>
</div>
);
}
if (status === 'success') {
return (
<div className="success">
<h2>Success!</h2>
<p>Redirecting to your dashboard...</p>
</div>
);
}
return (
<div className="error">
<h2>Verification Failed</h2>
<p>{errorMessage}</p>
<a href="/login">Request a new magic link</a>
</div>
);
}
Best Practices
Magic Link Security
- Short expiration times: Links should expire quickly (15 minutes recommended)
- One-time use: Tokens should be invalidated after first use
- Rate limiting: Prevent abuse by limiting magic link requests
- Clear email content: Make emails recognizable to prevent phishing
- Secure token generation: Use cryptographically secure random tokens
Troubleshooting
Magic Link Issues
“Magic link not received”
- Check spam/junk folder
- Verify email configuration in AuthOS
- Check rate limits
“Magic link expired”
- Links expire after 15 minutes by default
- User needs to request a new link
“Token already used”
- Magic links are single-use for security
- User needs to request a new link
Next steps
- Return to the passwordless mechanism chooser.
- Compare the Passkeys guide.
- Look up exact methods in the Magic Links SDK reference.