Skip to main content
POST
/
magiclink
curl -X POST "http://localhost:8080/magiclink" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "create_user": true
  }'
{
  "message_id": "msg_1234567890abcdef"
}
Send a magic link via email for passwordless authentication. Users can sign in by clicking the link in their email without entering a password.
This endpoint does not require authentication and can create new users if create_user is set to true.
curl -X POST "http://localhost:8080/magiclink" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "create_user": true
  }'

Request Body

email
string
required
Email address to send the magic link to.
create_user
boolean
Whether to create a new user if the email doesn’t exist. Defaults to false.
redirect_to
string
URL to redirect to after successful authentication. If not provided, uses the default redirect URL.
data
object
Additional user metadata to store if creating a new user.
captcha_token
string
Captcha token for verification if captcha is enabled.

Response

message_id
string
Unique identifier for the sent magic link email (when available)
{
  "message_id": "msg_1234567890abcdef"
}

Error Responses

{
  "code": 400,
  "msg": "Invalid email format",
  "details": "Please provide a valid email address"
}
1

Request Magic Link

User enters their email address and requests a magic link
2

Email Sent

A magic link email is sent to the user’s email address
3

User Clicks Link

User clicks the magic link in their email
4

Authentication

User is automatically authenticated and redirected to your application
<!DOCTYPE html>
<html>
<head>
  <title>Sign in to Strike</title>
</head>
<body>
  <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
    <h1>Sign in to Strike</h1>
    
    <p>Hi there,</p>
    
    <p>Click the button below to sign in to your Strike account:</p>
    
    <a href="{{.MagicLinkURL}}" 
       style="background-color: #0D9373; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
      Sign In
    </a>
    
    <p>Or copy and paste this link into your browser:</p>
    <p>{{.MagicLinkURL}}</p>
    
    <p>This link will expire in 1 hour for security reasons.</p>
    
    <p>If you didn't request this sign-in link, you can safely ignore this email.</p>
    
    <p>Best regards,<br>The Strike Team</p>
  </div>
</body>
</html>

Implementation Examples

import { useState } from 'react';

function MagicLinkForm() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [sent, setSent] = useState(false);
  const [error, setError] = useState('');
  const [createUser, setCreateUser] = useState(true);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/magiclink', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email,
          create_user: createUser,
          redirect_to: window.location.origin + '/dashboard'
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to send magic link');
      }

      setSent(true);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (sent) {
    return (
      <div className="magic-link-success">
        <h2>Check your email!</h2>
        <p>
          We've sent a magic link to <strong>{email}</strong>
        </p>
        <p>
          Click the link in your email to sign in instantly.
        </p>
        <p>
          <small>
            Didn't receive the email? Check your spam folder or{' '}
            <button 
              onClick={() => setSent(false)}
              className="link-button"
            >
              try again
            </button>
          </small>
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="magic-link-form">
      <h2>Sign in with Magic Link</h2>
      <p>
        Enter your email address and we'll send you a link to sign in instantly.
      </p>
      
      {error && (
        <div className="error-message">
          {error}
        </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="Enter your email"
          required
        />
      </div>

      <div className="form-group">
        <label className="checkbox-label">
          <input
            type="checkbox"
            checked={createUser}
            onChange={(e) => setCreateUser(e.target.checked)}
          />
          Create account if it doesn't exist
        </label>
      </div>

      <button type="submit" disabled={loading || !email}>
        {loading ? 'Sending...' : 'Send Magic Link'}
      </button>

      <p>
        Prefer a password? <a href="/login">Sign in with password</a>
      </p>
    </form>
  );
}

export default MagicLinkForm;
import { useState } from 'react';

function MagicLinkSignup() {
  const [formData, setFormData] = useState({
    email: '',
    firstName: '',
    lastName: ''
  });
  const [loading, setLoading] = useState(false);
  const [sent, setSent] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/magiclink', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: formData.email,
          create_user: true,
          data: {
            first_name: formData.firstName,
            last_name: formData.lastName
          },
          redirect_to: window.location.origin + '/welcome'
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to send magic link');
      }

      setSent(true);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleInputChange = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };

  if (sent) {
    return (
      <div className="signup-success">
        <h2>Welcome to Strike!</h2>
        <p>
          We've sent a magic link to <strong>{formData.email}</strong>
        </p>
        <p>
          Click the link to complete your account setup and sign in.
        </p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="magic-signup-form">
      <h2>Join Strike</h2>
      <p>
        Create your account and sign in instantly with a magic link.
      </p>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <div className="form-row">
        <div className="form-group">
          <label htmlFor="firstName">First Name</label>
          <input
            type="text"
            id="firstName"
            value={formData.firstName}
            onChange={(e) => handleInputChange('firstName', e.target.value)}
            required
          />
        </div>
        
        <div className="form-group">
          <label htmlFor="lastName">Last Name</label>
          <input
            type="text"
            id="lastName"
            value={formData.lastName}
            onChange={(e) => handleInputChange('lastName', e.target.value)}
            required
          />
        </div>
      </div>

      <div className="form-group">
        <label htmlFor="email">Email Address</label>
        <input
          type="email"
          id="email"
          value={formData.email}
          onChange={(e) => handleInputChange('email', e.target.value)}
          placeholder="Enter your email"
          required
        />
      </div>

      <button type="submit" disabled={loading}>
        {loading ? 'Creating Account...' : 'Create Account & Send Magic Link'}
      </button>

      <p>
        Already have an account? <a href="/login">Sign in</a>
      </p>
    </form>
  );
}

export default MagicLinkSignup;

Node.js Backend Handler

const express = require('express');
const { body, validationResult } = require('express-validator');

const router = express.Router();

router.post('/magiclink', [
  body('email').isEmail().normalizeEmail(),
  body('create_user').optional().isBoolean(),
  body('data').optional().isObject()
], async (req, res) => {
  try {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid request data',
        details: errors.array()
      });
    }

    const { email, create_user, redirect_to, data, captcha_token } = req.body;

    // Call Strike Auth Service
    const response = await fetch(`${process.env.AUTH_SERVICE_URL}/magiclink`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email,
        create_user: create_user || false,
        redirect_to: redirect_to || `${process.env.APP_URL}/dashboard`,
        data,
        captcha_token
      }),
    });

    const result = await response.json();

    if (!response.ok) {
      return res.status(response.status).json(result);
    }

    // Log magic link request
    console.log(`Magic link sent to: ${email}, create_user: ${create_user}`);

    res.json(result);

  } catch (error) {
    console.error('Magic link error:', error);
    res.status(500).json({
      code: 500,
      msg: 'Internal server error',
      details: 'Please try again later'
    });
  }
});

module.exports = router;

Use Cases

Passwordless Login

Perfect for users who prefer not to remember passwords:
{
  "email": "user@example.com",
  "create_user": false,
  "redirect_to": "https://yourapp.com/dashboard"
}

Quick Signup

Streamlined user registration without password requirements:
{
  "email": "newuser@example.com",
  "create_user": true,
  "data": {
    "first_name": "John",
    "last_name": "Doe",
    "marketing_consent": true
  },
  "redirect_to": "https://yourapp.com/welcome"
}

Account Recovery

Alternative to password reset for users who prefer magic links:
{
  "email": "user@example.com",
  "create_user": false,
  "redirect_to": "https://yourapp.com/account-recovered"
}

Security Features

  • Token Expiration: Magic links expire after 1 hour
  • Single Use: Links can only be used once
  • Rate Limiting: Prevents spam and abuse
  • Secure Tokens: Cryptographically secure random tokens
  • Email Verification: Inherent email verification through link click

Rate Limiting

This endpoint is rate limited to prevent abuse:
Limit TypeLimitWindow
Per Email3 requests5 minutes
Per IP10 requests10 minutes

Best Practices

  • Provide clear instructions about checking email
  • Include troubleshooting tips for email delivery issues
  • Offer alternative sign-in methods
  • Show loading states during email sending
  • Implement resend functionality with cooldown
  • Use secure, random tokens with sufficient entropy
  • Implement proper token expiration (1 hour recommended)
  • Log magic link requests for security monitoring
  • Validate email addresses before sending
  • Implement rate limiting to prevent abuse
  • Use reputable email service providers
  • Implement proper email authentication (SPF, DKIM, DMARC)
  • Monitor delivery rates and bounce rates
  • Provide clear sender identification
  • Include plain text version of emails

Testing

Unit Tests

describe('POST /magiclink', () => {
  test('should send magic link for existing user', async () => {
    const response = await request(app)
      .post('/magiclink')
      .send({
        email: 'existing@example.com',
        create_user: false
      })
      .expect(200);

    expect(response.body.message_id).toBeDefined();
  });

  test('should create user and send magic link', async () => {
    const response = await request(app)
      .post('/magiclink')
      .send({
        email: 'new@example.com',
        create_user: true,
        data: {
          first_name: 'Test',
          last_name: 'User'
        }
      })
      .expect(200);

    expect(response.body.message_id).toBeDefined();
  });

  test('should reject invalid email', async () => {
    await request(app)
      .post('/magiclink')
      .send({
        email: 'invalid-email',
        create_user: true
      })
      .expect(400);
  });

  test('should return 404 for non-existent user when create_user is false', async () => {
    await request(app)
      .post('/magiclink')
      .send({
        email: 'nonexistent@example.com',
        create_user: false
      })
      .expect(404);
  });
});
I