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 of passwordless session (currently only ‘MagicLink’ is supported)
Email address of the user to authenticate
URL to redirect the user after successful authentication
Optional state parameter to maintain state between the request and callback
Connection ID to use for authentication
Number of seconds until the magic link expires (default: 600)
Unique identifier for the passwordless session
Email address associated with the session
Expiration timestamp for the magic link
The magic link URL that authenticates the user
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.
The ID of the passwordless session to send
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
-
Set appropriate expiration times: Default is 10 minutes (600 seconds). Adjust based on your security requirements.
-
Use state parameter: Always use the state parameter to prevent CSRF attacks and maintain application state.
-
Validate emails: Ensure email addresses are valid before creating sessions to avoid unnecessary API calls.
-
Rate limiting: Implement rate limiting on your passwordless endpoint to prevent abuse.
-
Clear error messages: Provide user-friendly error messages without exposing sensitive information.
-
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
});