workos-magic-link
WorkOS Magic Link
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch these URLs in order:
https://workos.com/docs/magic-link/launch-checklisthttps://workos.com/docs/magic-link/indexhttps://workos.com/docs/magic-link/example-apps
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 withsk_(production or test)WORKOS_CLIENT_ID- starts withclient_
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
codeparameter (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:
1. Go to WorkOS Dashboard → Connections
2. Find "Magic Link" connection type
3. 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_uriparameter 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) orsk_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
More from workos/skills
workos
Use when the user asks for a WorkOS docs URL, term, or dashboard field (Sign-in endpoint, initiate_login_uri, Redirect URI, `WORKOS_*` env vars), or is implementing, debugging, or migrating WorkOS — AuthKit, SSO/SAML, Directory Sync, RBAC, FGA, MFA, Vault, Audit Logs, Admin Portal, Pipes (Connected Apps), Feature Flags, Radar (bot/fraud detection), webhooks, Custom Domains, or migrating from Auth0, Clerk, Cognito, Firebase, Supabase, Stytch, Descope, or Better Auth. Also triggers on @workos-inc/* imports.
594workos-widgets
Use when the user is implementing, embedding, or debugging a WorkOS Widget — specifically the User Management, User Profile, Admin Portal SSO Connection, or Admin Portal Domain Verification widgets. Handles the full stack — detecting the frontend (Next.js, React, React Router, TanStack Start, Vite, SvelteKit), generating access tokens via the backend SDK in use (Node, Python, Go, Ruby, PHP, Java, .NET), and wiring up the widget component correctly per the bundled OpenAPI spec. Also use when code imports from @workos-inc/widgets or the user pastes <UserManagement /> or <UserProfile /> JSX.
269workos-authkit-nextjs
Integrate WorkOS AuthKit with Next.js App Router (13+). Server-side rendering required.
64workos-authkit-base
Architectural reference for WorkOS AuthKit integrations. Fetch README first for implementation details.
42workos-authkit-react
Integrate WorkOS AuthKit with React single-page applications. Client-side only authentication. Use when the project is a React SPA without Next.js or React Router.
33workos-authkit-tanstack-start
Integrate WorkOS AuthKit with TanStack Start applications. Full-stack TypeScript with server functions. Use when project uses TanStack Start, @tanstack/start, or vinxi.
28