Authentication Foundation
Authentication (verifying identity) is critical security control. Weak authentication compromises everything else. Modern authentication moves beyond passwords to multi-factor flows balancing security and usability.
OAuth 2.0: Industry Standard
OAuth 2.0 enables third-party application access without sharing passwords.
Flow:
- User clicks "Login with Google"
- User redirected to Google login
- User authenticates with Google
- Google redirects back to your app with code
- Your app exchanges code for token
- Your app uses token to fetch user info
Advantages:
- Users trust large providers (Google, GitHub)
- No password storage for your app
- Rich user data from providers
- Easy integration via libraries
Disadvantages:
- Dependency on provider availability
- Limited user data if provider shares little
- Requires handling token refresh
import { OAuth2Client } from 'google-auth-library';
const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
'http://localhost:3000/auth/google/callback'
);
// Redirect to login
export function loginUrl() {
const scopes = ['email', 'profile'];
return oauth2Client.generateAuthUrl({ access_type: 'offline', scopes });
}
// Handle callback
export async function handleCallback(code: string) {
const { tokens } = await oauth2Client.getToken(code);
const ticket = await oauth2Client.verifyIdToken({
idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID
});
return ticket.getPayload();
}
SAML for Enterprise SSO
SAML (Security Assertion Markup Language) enables single sign-on within organizations.
Flow:
- Employee visits your app
- App redirects to company identity provider
- Employee logs in once
- IdP sends SAML assertion
- Your app validates assertion and logs in user
Advantages:
- Enterprise standard
- Centralized user management
- Reduced support for enterprise customers
- Compliance requirement for some enterprises
Disadvantages:
- Complex XML protocol
- Requires IdP on customer side
- Most common in enterprise, not consumer
SAML is essential for selling to enterprises. Most B2B SaaS support SAML SSO.
OpenID Connect: Modern Simplification
OpenID Connect (OIDC) layers identity on OAuth 2.0. Combines authentication and authorization.
Modern replacement for SAML: simpler, JSON-based, easier to implement.
import { Issuer } from 'openid-client';
// Discover provider configuration
const issuer = await Issuer.discover(providerUrl);
const client = new issuer.Client({
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
redirect_uris: ['http://localhost:3000/auth/callback']
});
// Generate login URL
const url = client.authorizationUrl({
scope: 'openid email profile'
});
// Handle callback
const params = client.callbackParams(req);
const tokenSet = await client.callback('http://localhost:3000/auth/callback', params);
const userInfo = await client.userinfo(tokenSet);
Passwordless Authentication
Passwordless eliminates passwords entirely.
Email link flow:
- User enters email
- Your app sends link with token
- User clicks link
- Your app validates token, logs in user
async function sendMagicLink(email: string) {
const token = generateSecureToken();
await db.insert('magic_links', {
email,
token,
expires_at: Date.now() + 15 * 60 * 1000 // 15 minutes
});
await sendEmail({
to: email,
subject: 'Login to your account',
body: `Click here to login: http://app.com/auth/link?token=${token}`
});
}
async function validateMagicLink(token: string) {
const link = await db.query(
'SELECT * FROM magic_links WHERE token = ? AND expires_at > ?',
[token, Date.now()]
);
if (!link) throw new Error('Invalid or expired link');
return link.email;
}
Passkeys flow: Use WebAuthn API. User registers biometric or security key. Login requires biometric.
Modern, most secure, but less supported than email links.
Multi-Factor Authentication
Combine password/passwordless with second factor:
TOTP (Time-based One-Time Password):
import speakeasy from 'speakeasy';
// Generate QR code during setup
function generateTOTPSecret(email: string) {
return speakeasy.generateSecret({
name: `YourApp (${email})`,
issuer: 'YourApp'
});
}
// Verify during login
function verifyTOTP(secret: string, token: string) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2
});
}
SMS OTP: Send 6-digit code to registered phone. User enters code to confirm.
Token Management
Securely manage tokens:
- Short-lived access tokens (15 minutes): JWT tokens with claims
- Long-lived refresh tokens (7 days): secure HTTP-only cookies
- Token rotation: issue new tokens regularly, invalidate old
// Issue tokens
function issueTokens(userId: string) {
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Verify access token
function verifyAccessToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET);
}
// Refresh expired token
function refreshAccessToken(refreshToken: string) {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
return issueTokens(payload.userId);
}
Frequently asked questions
Should we support username/password or force OAuth?
Support both initially. OAuth wins for consumer apps (lower friction). Password option essential for enterprise (IdP integration preference). Passwordless (email links) as third option. Let users choose—lowest friction wins adoption.
How do we handle users who lose access to second factor?
Provide backup codes generated during MFA setup. Users store safely; if device lost, use backup code to disable MFA. Implement recovery email allowing temporary bypass. Document recovery process for support.
What's the most secure authentication flow?
Passwordless + TOTP/passkeys combination. User authenticates with email magic link or passkey, then confirms with TOTP. Prevents password compromise (no passwords), phishing (no password harvesting), and token theft (MFA).