WorkOS Magic Link
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch these URLs in order:
These docs are the source of truth. If this skill conflicts with the documentation, follow the docs.
CRITICAL DEPRECATION NOTICE: Magic Link is deprecated by WorkOS due to email security software pre-visiting links and invalidating them. The docs recommend Magic Auth instead. Confirm with stakeholders before implementing Magic Link for new projects.
Step 2: Pre-Flight Validation
WorkOS Account Setup
-
Confirm WorkOS account exists at dashboard.workos.com
-
Navigate to Production environment → Settings → API Keys
-
Verify API key starts with sk_
-
Verify Client ID starts with client_
Environment Variables
Check .env or equivalent for:
-
WORKOS_API_KEY
-
starts with sk_ (production or test)
-
WORKOS_CLIENT_ID
-
starts with client_
Warning: Using test environment keys in production will fail. Verify environment matches deployment target.
Redirect URI Configuration
-
Check WorkOS Dashboard → Configuration → Redirect URIs
-
Confirm callback URL matches your app's domain
-
Format: https://yourdomain.com/auth/callback (HTTPS required for production)
Step 3: SDK Installation (Decision Tree)
Detect project language and install appropriate SDK:
Language/Framework? | +-- Node.js/Express --> npm install @workos-inc/node | +-- Python/Flask/Django --> pip install workos | +-- Ruby/Rails --> gem install workos | +-- Go --> go get github.com/workos/workos-go/v4 | +-- Java --> Maven/Gradle (see example apps URL)
Verify: SDK package exists in dependencies before continuing.
Check example apps documentation (from Step 1) for language-specific setup if not listed above.
Step 4: Initialize SDK
Configure SDK with API credentials. Pattern varies by language:
Node.js:
const { WorkOS } = require('@workos-inc/node');
const workos = new WorkOS(process.env.WORKOS_API_KEY); const clientId = process.env.WORKOS_CLIENT_ID;
Python:
import workos
workos.api_key = os.getenv('WORKOS_API_KEY') workos.client_id = os.getenv('WORKOS_CLIENT_ID')
Critical: Never hardcode credentials. Always use environment variables or secret management.
Verify SDK initialization:
Check SDK import doesn't throw
node -e "require('@workos-inc/node'); console.log('SDK loaded')"
Step 5: Create Callback Endpoint
Add a route to handle the OAuth callback from WorkOS. This endpoint:
-
Receives an authorization code parameter (valid 10 minutes)
-
Exchanges code for user Profile
-
Creates application session
-
Redirects to authenticated page
Route location (by framework):
Framework --> Route path Express/Node.js --> app.get('/auth/callback', ...) Flask/Django --> @app.route('/auth/callback') Rails --> get '/auth/callback'
Implementation pattern:
// Node.js/Express example - see docs for other languages app.get('/auth/callback', async (req, res) => { const { code } = req.query;
try { // Exchange code for user profile const profile = await workos.userManagement.authenticateWithCode({ code, clientId, });
// Create your application session
req.session.userId = profile.user.id;
req.session.email = profile.user.email;
// Redirect to app
res.redirect('/dashboard');
} catch (error) { console.error('Auth error:', error); res.redirect('/login?error=auth_failed'); } });
CRITICAL: The code expires after 10 minutes. Handle expiration errors gracefully.
Check the fetched documentation for exact SDK method names — they may vary by language and SDK version.
Step 6: Create Passwordless Session Endpoint
Add an endpoint that generates the Magic Link when user submits their email.
Key parameters:
-
email
-
User's email address (required)
-
redirect_uri
-
Override default callback URL (optional)
-
state
-
Arbitrary data to preserve between redirects (optional)
Implementation pattern:
app.post('/auth/magic-link', async (req, res) => { const { email } = req.body;
try { const session = await workos.passwordless.createSession({ email, type: 'MagicLink', redirectUri: 'https://yourdomain.com/auth/callback', state: JSON.stringify({ returnTo: req.body.returnTo }), });
// Decision: WorkOS email or custom email?
if (useWorkOSEmail) {
// WorkOS sends branded email automatically
await workos.passwordless.sendSession(session.id);
} else {
// Send via your email service
await yourEmailService.send({
to: email,
subject: 'Sign in to YourApp',
html: `<a href="${session.link}">Click to sign in</a>`,
});
}
res.json({ success: true });
} catch (error) { res.status(400).json({ error: error.message }); } });
Email delivery decision tree:
Email provider? | +-- Use WorkOS email --> Call sendSession(sessionId) | (WorkOS branded, no customization) | +-- Use custom email --> Send session.link via your service (Full template control, requires email infra)
CRITICAL - Magic Link expiration:
-
Links expire after 15 minutes
-
Links are single-use only
-
Email security software may pre-visit links and invalidate them before user clicks
-
If using corporate email (Gmail, Outlook), advise users to allowlist sender
Check documentation for exact SDK method names (createSession , sendSession ).
Step 7: Domain Connection Auto-Creation
Important: WorkOS automatically creates a Magic Link Connection when you create a Passwordless Session for a new email domain.
Verify in Dashboard:
- Go to WorkOS Dashboard → Connections
- Find "Magic Link" connection type
- Confirm domain appears after first session creation
No manual connection setup required. If domain doesn't appear, check API key permissions.
Step 8: Session Management (CRITICAL)
WorkOS does NOT manage sessions. You must implement:
-
Session creation - After code exchange in callback
-
Session storage - Cookie, JWT, database session, etc.
-
Session validation - Check on protected routes
-
Session expiration - Define timeout policy
-
Logout - Clear session data
Example session middleware:
// Protect routes requiring authentication function requireAuth(req, res, next) { if (!req.session.userId) { return res.redirect('/login'); } next(); }
app.get('/dashboard', requireAuth, (req, res) => { res.render('dashboard', { email: req.session.email }); });
Session duration: You decide. Common patterns:
-
Short (30 min - 1 hour) for high-security apps
-
Long (1-7 days) for consumer apps
-
Remember me checkbox for user choice
Step 9: Production Checklist
Before deploying to production, verify ALL items:
Dashboard Configuration:
-
Production environment unlocked (billing info added)
-
Production Redirect URI configured with HTTPS
-
Production API key secured (not committed to repo)
IP Allowlist (if required):
- Cloudflare IP ranges allowlisted: https://www.cloudflare.com/ips/
Code Verification:
-
Callback endpoint handles code exchange
-
Session creation works after authentication
-
Protected routes validate session
-
Logout clears session data
-
Error messages don't expose sensitive data
Email Deliverability:
-
Test email delivery in production domain
-
Configure SPF/DKIM if using custom email
-
Add sender to corporate allowlist (if applicable)
Verification Checklist (ALL MUST PASS)
Run these commands to verify integration:
1. Check SDK installed
npm list @workos-inc/node || pip list | grep workos || gem list | grep workos
2. Check environment variables set
env | grep WORKOS_API_KEY || echo "FAIL: API key not set" env | grep WORKOS_CLIENT_ID || echo "FAIL: Client ID not set"
3. Check callback route exists
grep -r "auth/callback" . --include=".js" --include=".py" --include="*.rb" || echo "FAIL: Callback route not found"
4. Check session management exists
grep -r "session" . --include=".js" --include=".py" --include="*.rb" | grep -v node_modules || echo "FAIL: No session management found"
5. Test endpoint responds (after server start)
curl -X POST http://localhost:3000/auth/magic-link
-H "Content-Type: application/json"
-d '{"email":"test@example.com"}' || echo "FAIL: Magic Link endpoint error"
6. Application builds/starts
npm run build || python app.py || rails server
If any check fails: Review corresponding step before proceeding.
Error Recovery
"Invalid authorization code" during callback
Root cause: Code expired (10 min limit) or already used.
Fix:
-
Check callback executes within 10 minutes of link click
-
Ensure callback doesn't retry/double-submit code
-
Add error handling to redirect user to re-authenticate: catch (error) { if (error.code === 'invalid_grant') { return res.redirect('/login?error=expired'); } }
"Magic Link already used" or link doesn't work
Root cause: Email security software pre-visited link.
Fix:
-
Confirm with user if they use corporate email (Gmail, Outlook, etc.)
-
Advise user to allowlist sender email
-
Consider switching to Magic Auth (not Magic Link) per WorkOS recommendation
-
Check email client settings for link preview/pre-fetch features
Reference: https://workos.com/docs/magic-link/index (deprecation notice section)
"Redirect URI mismatch"
Root cause: Callback URL doesn't match Dashboard configuration.
Fix:
-
Check WorkOS Dashboard → Configuration → Redirect URIs
-
Ensure exact match including protocol (https://) and path
-
If using redirect_uri parameter in createSession, verify it's pre-registered
-
Production must use HTTPS, not HTTP
"Invalid API key"
Root cause: Wrong environment key or key doesn't have permissions.
Fix:
-
Verify key starts with sk_test_ (test) or sk_live_ (production)
-
Check environment matches: test keys don't work in production
-
Regenerate key in Dashboard if compromised
SDK import errors
Root cause: Package not installed or wrong import path.
Fix by language:
Node.js
npm install @workos-inc/node
Check: const { WorkOS } = require('@workos-inc/node')
Python
pip install workos
Check: import workos
Ruby
gem install workos
Check: require 'workos'
Session not persisting after login
Root cause: Session middleware not configured or cookies not working.
Fix:
-
Verify session middleware initialized (express-session, Flask sessions, etc.)
-
Check cookie settings (httpOnly, secure, sameSite)
-
Ensure HTTPS in production (secure cookies require it)
-
Verify session secret is set
Email not delivered
Root cause: Email provider blocking or SPF/DKIM not configured.
Fix:
-
Check spam folder
-
If using WorkOS email: Check Dashboard logs for delivery status
-
If using custom email: Verify email service credentials
-
Configure SPF/DKIM records for custom domain
-
Test with personal email (Gmail) first to isolate corporate email issues
State Parameter Usage
The optional state parameter preserves application context across the auth flow:
Use cases:
-
Return user to original page after login
-
Pass through A/B test variant
-
Preserve shopping cart state
-
Track referral source
Pattern:
// Encode state when creating session const state = JSON.stringify({ returnTo: '/checkout', variant: 'experiment_a', });
await workos.passwordless.createSession({ email, type: 'MagicLink', state, });
// Decode state in callback const { state } = req.query; const { returnTo, variant } = JSON.parse(state); res.redirect(returnTo || '/dashboard');
Security note: Don't put sensitive data in state — it's visible in URL. Use it only for non-sensitive routing/tracking.
Related Skills
-
workos-authkit-nextjs: Full-featured auth with session management built-in
-
workos-magic-auth: Recommended replacement for Magic Link (code-based instead of link-based)
-
workos-sso: Enterprise SSO using same redirect flow as Magic Link
-
workos-mfa: Add second factor to passwordless flows