Passwordless Authentication
This guide demonstrates how to implement passwordless authentication using passkeys (WebAuthn/FIDO2) and magic links. Passwordless authentication improves security by eliminating password-related vulnerabilities and provides a better user experience.
Overview
AuthOS supports two passwordless authentication methods:
- Passkeys (WebAuthn) - Biometric authentication using Touch ID, Face ID, Windows Hello, or hardware security keys
- Magic Links - One-time email links for authentication without passwords
Both methods eliminate the need for users to remember passwords while providing strong security guarantees.
Prerequisites
Before implementing passwordless authentication:
- Install the SDK:
npm install @drmhse/sso-sdk - Initialize the SSO client with your platform URL
- Ensure your application uses HTTPS (required for WebAuthn)
Passkeys (WebAuthn) Authentication
Passkeys use the WebAuthn standard to authenticate users with biometrics or hardware security keys. This provides phishing-resistant authentication with an excellent user experience.
Browser Compatibility Check
Before implementing passkeys, check if the user’s browser supports WebAuthn:
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
// Check if WebAuthn is supported
if (!sso.passkeys.isSupported()) {
console.log('Passkeys not supported - show password login instead');
return;
}
// Check if platform authenticator is available (Touch ID, Face ID, etc.)
const hasPlatformAuth = await sso.passkeys.isPlatformAuthenticatorAvailable();
if (hasPlatformAuth) {
console.log('Biometric authentication available');
}
Registering a Passkey
Users must register a passkey while authenticated. This is typically done from account settings after the user has logged in with another method.
async function registerPasskey() {
// User must be authenticated first
const accessToken = localStorage.getItem('sso_access_token');
if (!accessToken) {
throw new Error('User must be logged in to register a passkey');
}
sso.setAuthToken(accessToken);
try {
// Register the passkey with a friendly name
const passkeyId = await sso.passkeys.register('My MacBook Pro');
console.log('Passkey registered successfully:', passkeyId);
// Show success message to user
return passkeyId;
} catch (error) {
console.error('Passkey registration failed:', error);
// Handle errors: user cancelled, authenticator not available, etc.
throw error;
}
}
Logging In with a Passkey
Once registered, users can authenticate using their passkey:
async function loginWithPasskey(email: string) {
try {
// Initiate passkey authentication
const result = await sso.passkeys.login(email);
// Store the token
localStorage.setItem('sso_access_token', result.token);
sso.setAuthToken(result.token);
console.log('Logged in as:', result.user_id);
// Redirect to dashboard
window.location.href = '/dashboard';
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('no passkeys registered')) {
// User has no passkeys - offer alternative login
console.error('No passkeys registered for this account');
} else {
console.error('Passkey login failed:', error.message);
}
}
throw error;
}
}
Complete React Passkey Implementation
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL!
});
export function PasskeyLogin() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSupported, setIsSupported] = useState(true);
// Check browser support on mount
useEffect(() => {
setIsSupported(sso.passkeys.isSupported());
}, []);
const handlePasskeyLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
const result = await sso.passkeys.login(email);
// Store token and redirect
localStorage.setItem('sso_access_token', result.token);
sso.setAuthToken(result.token);
window.location.href = '/dashboard';
} catch (err) {
setIsLoading(false);
if (err instanceof SsoApiError) {
if (err.statusCode === 404) {
setError('No passkeys registered for this account. Please use another login method.');
} else {
setError(err.message);
}
} else if (err instanceof Error) {
if (err.name === 'NotAllowedError') {
setError('Authentication cancelled or timed out');
} else {
setError('Passkey authentication failed');
}
}
}
};
if (!isSupported) {
return (
<div className="alert alert-warning">
Your browser does not support passkeys. Please use a modern browser or try password login.
</div>
);
}
return (
<div className="passkey-login">
<h2>Sign in with Passkey</h2>
<p>Use Touch ID, Face ID, or your security key to sign in securely</p>
<form onSubmit={handlePasskeyLogin}>
<div className="form-group">
<label>Email Address</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Authenticating...' : 'Sign in with Passkey'}
</button>
</form>
{error && <div className="alert alert-error">{error}</div>}
<div className="alternative-methods">
<a href="/login/password">Sign in with password instead</a>
</div>
</div>
);
}
export function PasskeyRegistration() {
const [deviceName, setDeviceName] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess(false);
setIsLoading(true);
try {
// User must already be authenticated
const passkeyId = await sso.passkeys.register(deviceName || undefined);
setSuccess(true);
setDeviceName('');
console.log('Registered passkey:', passkeyId);
} catch (err) {
setIsLoading(false);
if (err instanceof Error) {
if (err.name === 'NotAllowedError') {
setError('Registration cancelled or not allowed');
} else if (err.name === 'InvalidStateError') {
setError('A passkey is already registered on this device');
} else {
setError(err.message);
}
}
}
};
if (!sso.passkeys.isSupported()) {
return (
<div className="alert alert-warning">
Your browser does not support passkeys
</div>
);
}
return (
<div className="passkey-registration">
<h3>Register a Passkey</h3>
<p>Add a passkey to sign in faster and more securely</p>
{success ? (
<div className="alert alert-success">
Passkey registered successfully! You can now use it to sign in.
</div>
) : (
<form onSubmit={handleRegister}>
<div className="form-group">
<label>Device Name (Optional)</label>
<input
type="text"
value={deviceName}
onChange={(e) => setDeviceName(e.target.value)}
placeholder="e.g., My MacBook Pro"
/>
<small>Give this passkey a name to identify which device it's on</small>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Registering...' : 'Register Passkey'}
</button>
</form>
)}
{error && <div className="alert alert-error">{error}</div>}
</div>
);
}
Magic Link Authentication
Magic links provide passwordless authentication by sending one-time login links via email. This is simpler to implement than passkeys and works on any device with email access.
Requesting a Magic Link
async function requestMagicLink(email: string, orgSlug?: string) {
try {
const response = await sso.magicLinks.request({
email,
orgSlug // Optional: organization context
});
console.log(response.message); // "Magic link sent to your email"
return true;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 429) {
// Rate limit exceeded
console.error('Too many requests. Please wait before trying again.');
} else {
console.error('Failed to send magic link:', error.message);
}
}
return false;
}
}
Verifying a Magic Link
When users click the magic link in their email, your application needs to verify the token and complete authentication:
async function verifyMagicLink() {
// Extract token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
console.error('No token provided');
window.location.href = '/login';
return;
}
try {
// Verify the token
const auth = await sso.magicLinks.verify(token);
// Store tokens
localStorage.setItem('sso_access_token', auth.access_token);
localStorage.setItem('sso_refresh_token', auth.refresh_token);
sso.setAuthToken(auth.access_token);
// Redirect to application
window.location.href = '/dashboard';
} catch (error) {
if (error instanceof SsoApiError) {
if (error.statusCode === 404 || error.statusCode === 410) {
console.error('Magic link has expired or been used');
} else {
console.error('Verification failed:', error.message);
}
}
// Redirect back to login on error
window.location.href = '/login?error=invalid_link';
}
}
Complete React Magic Link Implementation
import { useState } from 'react';
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL!
});
export function MagicLinkLogin() {
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
await sso.magicLinks.request({
email,
orgSlug: 'acme-corp' // Optional organization context
});
setSubmitted(true);
} catch (err) {
setIsLoading(false);
if (err instanceof SsoApiError) {
if (err.statusCode === 429) {
setError('Too many requests. Please wait a moment and try again.');
} else {
setError('Failed to send magic link. Please try again.');
}
}
}
};
if (submitted) {
return (
<div className="magic-link-sent">
<h2>Check your email</h2>
<p>
We sent a magic link to <strong>{email}</strong>
</p>
<p>Click the link in the email to sign in. The link expires in 15 minutes.</p>
<button onClick={() => setSubmitted(false)}>
Use a different email
</button>
</div>
);
}
return (
<div className="magic-link-login">
<h2>Sign in with Magic Link</h2>
<p>Enter your email address and we'll send you a link to sign in</p>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email Address</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
autoFocus
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
{error && <div className="alert alert-error">{error}</div>}
<div className="alternative-methods">
<a href="/login/password">Sign in with password instead</a>
</div>
</div>
);
}
export function MagicLinkVerify() {
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying');
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
verifyToken();
}, []);
const verifyToken = async () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
setStatus('error');
setErrorMessage('No verification token found');
return;
}
try {
const auth = await sso.magicLinks.verify(token);
// Store tokens
localStorage.setItem('sso_access_token', auth.access_token);
localStorage.setItem('sso_refresh_token', auth.refresh_token);
sso.setAuthToken(auth.access_token);
setStatus('success');
// Redirect after brief success message
setTimeout(() => {
window.location.href = '/dashboard';
}, 1500);
} catch (error) {
setStatus('error');
if (error instanceof SsoApiError) {
if (error.statusCode === 404) {
setErrorMessage('This magic link has expired or been used');
} else if (error.statusCode === 410) {
setErrorMessage('This magic link has already been used');
} else {
setErrorMessage('Verification failed. Please request a new link.');
}
} else {
setErrorMessage('An unexpected error occurred');
}
}
};
if (status === 'verifying') {
return (
<div className="verifying">
<h2>Verifying your magic link...</h2>
<div className="spinner"></div>
</div>
);
}
if (status === 'success') {
return (
<div className="success">
<h2>Success!</h2>
<p>Redirecting to your dashboard...</p>
</div>
);
}
return (
<div className="error">
<h2>Verification Failed</h2>
<p>{errorMessage}</p>
<a href="/login">Request a new magic link</a>
</div>
);
}
Best Practices
Passkey Security
- Always use HTTPS: WebAuthn requires a secure context
- Prevent account lockout: Always offer alternative authentication methods
- User-friendly device names: Encourage users to name their passkeys
- Handle errors gracefully: Users may cancel authentication or not have a compatible device
- Support multiple passkeys: Allow users to register passkeys on multiple devices
Magic Link Security
- Short expiration times: Links should expire quickly (15 minutes recommended)
- One-time use: Tokens should be invalidated after first use
- Rate limiting: Prevent abuse by limiting magic link requests
- Clear email content: Make emails recognizable to prevent phishing
- Secure token generation: Use cryptographically secure random tokens
User Experience
- Progressive enhancement: Detect browser capabilities and offer appropriate methods
- Clear instructions: Guide users through the authentication process
- Alternative methods: Always provide fallback authentication options
- Loading states: Show clear feedback during authentication
- Error messages: Provide actionable error messages
Implementation Checklist
- Check browser support before showing passkey options
- Implement proper error handling for WebAuthn exceptions
- Configure appropriate magic link expiration times
- Add rate limiting for magic link requests
- Test on multiple devices and browsers
- Provide clear user instructions
- Implement fallback authentication methods
- Monitor authentication success rates
- Handle edge cases (expired links, cancelled authentication)
Combining Methods
For the best user experience, combine multiple passwordless methods:
export function UnifiedPasswordlessLogin() {
const [email, setEmail] = useState('');
const [method, setMethod] = useState<'passkey' | 'magic_link'>('passkey');
const hasPasskeySupport = sso.passkeys.isSupported();
// Auto-select magic link if passkeys not supported
useEffect(() => {
if (!hasPasskeySupport) {
setMethod('magic_link');
}
}, [hasPasskeySupport]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (method === 'passkey' && hasPasskeySupport) {
// Try passkey login
try {
const result = await sso.passkeys.login(email);
// Handle success
} catch (error) {
// Fallback to magic link on error
console.log('Passkey failed, sending magic link');
await sso.magicLinks.request({ email });
}
} else {
// Use magic link
await sso.magicLinks.request({ email });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
{hasPasskeySupport && (
<div className="method-selector">
<label>
<input
type="radio"
checked={method === 'passkey'}
onChange={() => setMethod('passkey')}
/>
Use Passkey (Touch ID / Face ID)
</label>
<label>
<input
type="radio"
checked={method === 'magic_link'}
onChange={() => setMethod('magic_link')}
/>
Email me a magic link
</label>
</div>
)}
<button type="submit">
{method === 'passkey' ? 'Sign in with Passkey' : 'Send Magic Link'}
</button>
</form>
);
}
Troubleshooting
Passkey Issues
“WebAuthn not supported”
- Ensure the site is accessed via HTTPS (localhost is exempt)
- Check browser compatibility (Chrome 67+, Safari 14+, Firefox 60+)
- Verify user has a compatible authenticator
“Registration failed”
- User may have cancelled the operation
- Authenticator might already have a credential for this site
- Check for conflicting browser extensions
“Authentication failed”
- User may have no registered passkeys
- Passkey might have been removed from the authenticator
- Network or server errors
Magic Link Issues
“Magic link not received”
- Check spam/junk folder
- Verify email configuration on the SSO platform
- Check rate limits
“Magic link expired”
- Links expire after 15 minutes by default
- User needs to request a new link
“Token already used”
- Magic links are single-use for security
- User needs to request a new link
Related Documentation
- Authentication Module Reference - Complete API reference
- Authentication Flows Guide - OAuth and token management
- Password Authentication Guide - Traditional password flows