Implement Magic Links

Implement AuthOS email magic-link request and verification flows

AuthOS release 0.8.2 TypeScript SDK 0.8.0 Latest-only documentation
Updated Jul 15, 2026 Edit this page
On this page

Complete the passwordless prerequisites and choose magic links only after testing email delivery, expiry, replay, and recovery behavior.

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.

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;
  }
}

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';
  }
}
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

  1. Short expiration times: Links should expire quickly (15 minutes recommended)
  2. One-time use: Tokens should be invalidated after first use
  3. Rate limiting: Prevent abuse by limiting magic link requests
  4. Clear email content: Make emails recognizable to prevent phishing
  5. Secure token generation: Use cryptographically secure random tokens

Troubleshooting

“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