Skip to main content
The Passwordless class provides methods for creating and sending passwordless authentication sessions using magic links.

Overview

Passwordless authentication allows users to sign in without entering a password. Instead, they receive a magic link via email that authenticates them when clicked.

Methods

createSession

Creates a new passwordless authentication session and generates a magic link.
type
'MagicLink'
required
Type of passwordless session (currently only ‘MagicLink’ is supported)
email
string
required
Email address of the user to authenticate
redirectURI
string
URL to redirect the user after successful authentication
state
string
Optional state parameter to maintain state between the request and callback
connection
string
Connection ID to use for authentication
expiresIn
number
Number of seconds until the magic link expires (default: 600)
session
PasswordlessSession
id
string
Unique identifier for the passwordless session
email
string
Email address associated with the session
expiresAt
Date
Expiration timestamp for the magic link
The magic link URL that authenticates the user
object
'passwordless_session'
Object type identifier
const session = await workos.passwordless.createSession({
  type: 'MagicLink',
  email: '[email protected]',
  redirectURI: 'https://myapp.com/auth/callback',
  expiresIn: 600 // 10 minutes
});

console.log('Magic link:', session.link);
console.log('Expires at:', session.expiresAt);

sendSession

Sends the magic link email to the user. WorkOS handles the email delivery.
sessionId
string
required
The ID of the passwordless session to send
response
SendSessionResponse
success
boolean
Whether the email was sent successfully
const response = await workos.passwordless.sendSession(
  'passwordless_session_01HZXK8F9P3QZJQ2Z1Z2Z3Z4Z5'
);

if (response.success) {
  console.log('Magic link email sent successfully');
}

Complete Passwordless Flow

Basic Implementation

import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS(process.env.WORKOS_API_KEY);

// Step 1: Create and send magic link
app.post('/auth/passwordless', async (req, res) => {
  const { email } = req.body;
  
  try {
    // Create the passwordless session
    const session = await workos.passwordless.createSession({
      type: 'MagicLink',
      email,
      redirectURI: 'https://myapp.com/auth/callback',
      state: generateRandomState(),
      expiresIn: 600 // 10 minutes
    });
    
    // Send the magic link email
    await workos.passwordless.sendSession(session.id);
    
    res.json({
      success: true,
      message: 'Magic link sent to your email'
    });
  } catch (error) {
    console.error('Passwordless auth error:', error);
    res.status(500).json({ error: 'Failed to send magic link' });
  }
});

// Step 2: Handle the callback when user clicks magic link
app.get('/auth/callback', async (req, res) => {
  const { code } = req.query;
  
  try {
    // Exchange the code for user information
    const { user, accessToken } = await workos.userManagement.authenticateWithCode({
      code,
      clientId: process.env.WORKOS_CLIENT_ID
    });
    
    // Create session for the user
    req.session.userId = user.id;
    req.session.email = user.email;
    
    res.redirect('/dashboard');
  } catch (error) {
    console.error('Callback error:', error);
    res.redirect('/login?error=auth_failed');
  }
});

Custom Email Delivery

If you want to send the magic link through your own email service instead of using WorkOS email delivery:
app.post('/auth/passwordless', async (req, res) => {
  const { email } = req.body;
  
  // Create the session but don't use sendSession()
  const session = await workos.passwordless.createSession({
    type: 'MagicLink',
    email,
    redirectURI: 'https://myapp.com/auth/callback',
    expiresIn: 600
  });
  
  // Send the magic link using your own email service
  await yourEmailService.send({
    to: email,
    subject: 'Sign in to MyApp',
    html: `
      <h1>Sign in to MyApp</h1>
      <p>Click the link below to sign in:</p>
      <a href="${session.link}">Sign In</a>
      <p>This link expires at ${session.expiresAt.toLocaleString()}</p>
    `
  });
  
  res.json({ success: true });
});

With State Management

Use the state parameter to maintain state between the authentication request and callback:
app.post('/auth/passwordless', async (req, res) => {
  const { email, returnTo } = req.body;
  
  // Generate a unique state value
  const state = generateRandomState();
  
  // Store state with associated data (e.g., in Redis)
  await redis.setex(`auth_state:${state}`, 600, JSON.stringify({
    returnTo: returnTo || '/dashboard',
    createdAt: Date.now()
  }));
  
  const session = await workos.passwordless.createSession({
    type: 'MagicLink',
    email,
    redirectURI: 'https://myapp.com/auth/callback',
    state,
    expiresIn: 600
  });
  
  await workos.passwordless.sendSession(session.id);
  
  res.json({ success: true });
});

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Retrieve and verify state
  const stateData = await redis.get(`auth_state:${state}`);
  if (!stateData) {
    return res.status(400).send('Invalid or expired state');
  }
  
  const { returnTo } = JSON.parse(stateData);
  
  // Clean up state
  await redis.del(`auth_state:${state}`);
  
  const { user, accessToken } = await workos.userManagement.authenticateWithCode({
    code,
    clientId: process.env.WORKOS_CLIENT_ID
  });
  
  req.session.userId = user.id;
  
  // Redirect to the original destination
  res.redirect(returnTo);
});

With Organization-Specific Authentication

app.post('/auth/passwordless/:orgId', async (req, res) => {
  const { orgId } = req.params;
  const { email } = req.body;
  
  // Get organization's SSO connection
  const connections = await workos.sso.listConnections({
    organizationId: orgId
  });
  
  const connection = connections.data[0];
  
  const session = await workos.passwordless.createSession({
    type: 'MagicLink',
    email,
    redirectURI: `https://myapp.com/auth/callback?org=${orgId}`,
    connection: connection?.id, // Use organization's connection if available
    expiresIn: 600
  });
  
  await workos.passwordless.sendSession(session.id);
  
  res.json({ success: true });
});

Error Handling

try {
  const session = await workos.passwordless.createSession({
    type: 'MagicLink',
    email: '[email protected]',
    redirectURI: 'https://myapp.com/callback'
  });
  
  await workos.passwordless.sendSession(session.id);
} catch (error) {
  if (error.code === 'invalid_email') {
    console.error('Invalid email address');
  } else if (error.code === 'rate_limit_exceeded') {
    console.error('Too many requests, please try again later');
  } else {
    console.error('Passwordless auth error:', error.message);
  }
}

Best Practices

  1. Set appropriate expiration times: Default is 10 minutes (600 seconds). Adjust based on your security requirements.
  2. Use state parameter: Always use the state parameter to prevent CSRF attacks and maintain application state.
  3. Validate emails: Ensure email addresses are valid before creating sessions to avoid unnecessary API calls.
  4. Rate limiting: Implement rate limiting on your passwordless endpoint to prevent abuse.
  5. Clear error messages: Provide user-friendly error messages without exposing sensitive information.
  6. Monitor sessions: Track failed authentication attempts and expired sessions for security monitoring.
// Example with rate limiting
const rateLimit = require('express-rate-limit');

const passwordlessLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 3, // 3 requests per window
  message: 'Too many magic link requests, please try again later'
});

app.post('/auth/passwordless', passwordlessLimiter, async (req, res) => {
  // Your passwordless logic here
});

Build docs developers (and LLMs) love