Authentication is the foundation of application security, yet it's one of the most frequently mishandled aspects of software development. With credential-based attacks accounting for over 80% of data breaches and costing organizations an average of $4.45 million per incident, getting authentication right isn't just good practice—it's business critical.
After scaling a Customer Identity and Access Management (CIAM) platform to serve over 1 billion users globally, I've seen firsthand how authentication implementation makes or breaks enterprise deals. In fact, authentication requirements block 75-80% of B2B SaaS sales conversations. The difference between successful implementation and failure often comes down to understanding not just the protocols, but the practical patterns that work at scale.
This guide provides a complete roadmap for implementing production-grade authentication in modern applications. Whether you're building a consumer app, enterprise SaaS platform, or microservices architecture, you'll find actionable patterns and code examples to implement secure authentication that scales.
In this comprehensive guide, I'll cover:
Let's dive in.
Before jumping into implementation, it's crucial to understand the authentication protocol ecosystem and when to use each approach. The modern authentication landscape includes several key protocols, each designed for specific use cases.
Authentication answers "who are you?" by verifying a user's identity through credentials, biometrics, or other proof factors. Authorization answers "what can you do?" by determining which resources an authenticated user can access.
This distinction matters because many developers confuse the two, leading to security vulnerabilities. For a deeper understanding of how these concepts work together in modern applications, check out my comprehensive authentication and authorization security framework.
The authentication protocol landscape can be confusing. Here's a practical decision framework:
OAuth 2.0 + OpenID Connect (OIDC)
SAML 2.0
JSON Web Tokens (JWT)
Passkeys/WebAuthn
For a detailed technical comparison of these protocols, I've written an in-depth guide on JWT, OAuth, OIDC, and SAML that covers the strengths and tradeoffs of each approach.
One of the most common questions I get is: "Should I implement OIDC or SAML for enterprise authentication?"
The short answer: OIDC for new implementations, SAML when integrating with existing enterprise identity providers.
OIDC is built on OAuth 2.0, uses lightweight JSON tokens, and is designed for modern web and mobile applications. SAML is older, uses verbose XML, but has deep penetration in enterprise environments—especially with established IdPs like Active Directory Federation Services (ADFS) and Okta.
For a comprehensive technical comparison that will help you make the right choice, read my OIDC vs SAML deep dive.
Before writing a single line of code, you need to answer a fundamental question: should you build authentication in-house or use a CIAM provider?
Pros:
Cons:
Pros:
Cons:
Let's break down the actual costs:
Building In-House:
CIAM Provider:
For most startups and small teams, buying makes sense until you reach significant scale or have highly specialized requirements. If you're evaluating CIAM providers, I maintain a comprehensive directory of CIAM providers with detailed comparisons.
My recommendation: Start with a CIAM provider for initial launch, then consider building custom authentication once you have product-market fit and dedicated security resources.
While passwordless is the future, password-based authentication isn't going away anytime soon. If you're implementing password auth, here's how to do it securely.
This should go without saying, but I still see it in production applications. Never store passwords in plaintext or using reversible encryption. Always use a slow, adaptive hashing algorithm.
const argon2 = require('argon2');
// Hashing a password during registration
async function hashPassword(plainPassword) {
try {
const hash = await argon2.hash(plainPassword, {
type: argon2.argon2id, // Hybrid of argon2i and argon2d
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4 // 4 parallel threads
});
return hash;
} catch (err) {
throw new Error('Password hashing failed');
}
}
// Verifying password during login
async function verifyPassword(plainPassword, hashedPassword) {
try {
return await argon2.verify(hashedPassword, plainPassword);
} catch (err) {
return false;
}
}
// Usage example
async function registerUser(email, password) {
// Validate password strength first
if (!isPasswordStrong(password)) {
throw new Error('Password does not meet requirements');
}
const passwordHash = await hashPassword(password);
// Store user with hashed password
await db.users.create({
email: email,
password_hash: passwordHash,
created_at: new Date()
});
}
Why Argon2? It's the winner of the Password Hashing Competition (2015) and designed to resist both GPU and ASIC attacks. It's memory-hard, making brute-force attacks extremely expensive.
Alternative: bcrypt is also acceptable if Argon2 isn't available in your stack:
const bcrypt = require('bcrypt');
const saltRounds = 12; // Increase as hardware improves
const hash = await bcrypt.hash(plainPassword, saltRounds);
const isValid = await bcrypt.compare(plainPassword, hash);
Follow NIST 800-63B guidelines for password requirements:
const zxcvbn = require('zxcvbn');
const axios = require('axios');
async function isPasswordStrong(password) {
// Check minimum length
if (password.length < 12) {
return {
valid: false,
message: 'Password must be at least 12 characters'
};
}
// Check password strength with zxcvbn
const strength = zxcvbn(password);
if (strength.score < 3) {
return {
valid: false,
message: 'Password is too weak. ' + strength.feedback.warning
};
}
// Check if password has been compromised (HaveIBeenPwned)
const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
const prefix = sha1.substring(0, 5);
const suffix = sha1.substring(5);
const response = await axios.get(`https://api.pwnedpasswords.com/range/${prefix}`);
const hashes = response.data.split('\n');
for (const hash of hashes) {
const [hashSuffix, count] = hash.split(':');
if (hashSuffix === suffix) {
return {
valid: false,
message: `This password has been exposed in ${count} data breaches. Please choose a different password.`
};
}
}
return { valid: true, message: 'Password meets requirements' };
}
Password reset is often the weakest link in authentication systems. Here's a secure implementation:
const crypto = require('crypto');
async function initiatePasswordReset(email) {
const user = await db.users.findOne({ email });
if (!user) {
// Don't reveal if email exists (prevent enumeration)
return { success: true };
}
// Generate cryptographically secure reset token
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenHash = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// Store hashed token with expiration
await db.users.update(user.id, {
reset_token_hash: resetTokenHash,
reset_token_expires: new Date(Date.now() + 3600000) // 1 hour
});
// Send reset email
const resetUrl = `https://yourapp.com/reset-password?token=${resetToken}`;
await sendEmail(email, 'Password Reset', `Click here to reset: ${resetUrl}`);
return { success: true };
}
async function completePasswordReset(token, newPassword) {
// Hash the provided token
const resetTokenHash = crypto
.createHash('sha256')
.update(token)
.digest('hex');
// Find user with valid token
const user = await db.users.findOne({
reset_token_hash: resetTokenHash,
reset_token_expires: { $gt: new Date() }
});
if (!user) {
throw new Error('Invalid or expired reset token');
}
// Validate new password
const validation = await isPasswordStrong(newPassword);
if (!validation.valid) {
throw new Error(validation.message);
}
// Hash new password
const newPasswordHash = await hashPassword(newPassword);
// Update password and clear reset token
await db.users.update(user.id, {
password_hash: newPasswordHash,
reset_token_hash: null,
reset_token_expires: null,
password_changed_at: new Date()
});
// Invalidate all existing sessions
await db.sessions.deleteMany({ user_id: user.id });
return { success: true };
}
Key Security Points:
Multi-factor authentication reduces account takeover risk by 99.9% according to Microsoft. If you're handling sensitive data or enterprise customers, MFA is non-negotiable.
TOTP is the most common MFA method, supported by authenticator apps like Google Authenticator, Authy, and 1Password.
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
async function enableMFA(userId, username) {
// Generate secret
const secret = speakeasy.generateSecret({
name: `YourApp (${username})`,
length: 32
});
// Store secret (encrypted!) in database
await db.users.update(userId, {
mfa_secret: encryptSecret(secret.base32),
mfa_enabled: false // User must verify before enabling
});
// Generate QR code for authenticator app
const qrCodeDataUrl = await QRCode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32, // Show to user as backup
qrCode: qrCodeDataUrl
};
}
async function verifyMFASetup(userId, token) {
const user = await db.users.findById(userId);
if (!user.mfa_secret) {
throw new Error('MFA not initialized');
}
const decryptedSecret = decryptSecret(user.mfa_secret);
// Verify the token
const verified = speakeasy.totp.verify({
secret: decryptedSecret,
encoding: 'base32',
token: token,
window: 1 // Allow 1 time step before/after for clock drift
});
if (!verified) {
throw new Error('Invalid verification code');
}
// Enable MFA
await db.users.update(userId, {
mfa_enabled: true,
mfa_backup_codes: generateBackupCodes() // For account recovery
});
return { success: true };
}
async function verifyMFALogin(userId, token) {
const user = await db.users.findById(userId);
if (!user.mfa_enabled) {
return { valid: false, reason: 'MFA not enabled' };
}
const decryptedSecret = decryptSecret(user.mfa_secret);
// Verify TOTP
const verified = speakeasy.totp.verify({
secret: decryptedSecret,
encoding: 'base32',
token: token,
window: 1
});
if (verified) {
return { valid: true };
}
// Check if it's a backup code
if (user.mfa_backup_codes && user.mfa_backup_codes.includes(token)) {
// Remove used backup code
await db.users.update(userId, {
mfa_backup_codes: user.mfa_backup_codes.filter(code => code !== token)
});
return { valid: true, usedBackupCode: true };
}
return { valid: false, reason: 'Invalid code' };
}
function generateBackupCodes() {
const codes = [];
for (let i = 0; i < 10; i++) {
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
codes.push(code);
}
return codes;
}
Important: Always provide backup codes for account recovery. Users who lose their authenticator app need a way back in.
Passwordless authentication eliminates the weakest link in security: the password itself. With passkeys and WebAuthn gaining widespread browser support, now is the perfect time to implement passwordless auth.
I've written extensively about the shift to passwordless in WebAuthn: Passwordless Auth & Passkeys. The technology is mature and ready for production.
Here's a complete passkey implementation for registration and authentication:
Client-Side Registration:
// Register a new passkey
async function registerPasskey(username) {
try {
// Request registration options from server
const optionsResponse = await fetch('/api/auth/passkey/register-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await optionsResponse.json();
// Convert challenge and user ID from base64url
options.publicKey.challenge = base64urlDecode(options.publicKey.challenge);
options.publicKey.user.id = base64urlDecode(options.publicKey.user.id);
// Create credential using WebAuthn API
const credential = await navigator.credentials.create({
publicKey: options.publicKey
});
// Prepare credential for server
const credentialData = {
id: credential.id,
rawId: base64urlEncode(credential.rawId),
type: credential.type,
response: {
clientDataJSON: base64urlEncode(credential.response.clientDataJSON),
attestationObject: base64urlEncode(credential.response.attestationObject)
}
};
// Send credential to server for verification
const registerResponse = await fetch('/api/auth/passkey/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialData)
});
return await registerResponse.json();
} catch (error) {
console.error('Passkey registration failed:', error);
throw error;
}
}
// Authenticate with passkey
async function authenticateWithPasskey() {
try {
// Request authentication options from server
const optionsResponse = await fetch('/api/auth/passkey/auth-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const options = await optionsResponse.json();
// Convert challenge from base64url
options.publicKey.challenge = base64urlDecode(options.publicKey.challenge);
// Get credential
const credential = await navigator.credentials.get({
publicKey: options.publicKey
});
// Prepare credential for server
const credentialData = {
id: credential.id,
rawId: base64urlEncode(credential.rawId),
type: credential.type,
response: {
clientDataJSON: base64urlEncode(credential.response.clientDataJSON),
authenticatorData: base64urlEncode(credential.response.authenticatorData),
signature: base64urlEncode(credential.response.signature),
userHandle: base64urlEncode(credential.response.userHandle)
}
};
// Send to server for verification
const authResponse = await fetch('/api/auth/passkey/authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialData)
});
const result = await authResponse.json();
if (result.success) {
// Store session token
localStorage.setItem('session_token', result.token);
}
return result;
} catch (error) {
console.error('Passkey authentication failed:', error);
throw error;
}
}
// Base64url encoding/decoding helpers
function base64urlEncode(buffer) {
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function base64urlDecode(base64url) {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(base64);
return Uint8Array.from(binary, c => c.charCodeAt(0));
}
Server-Side Implementation (Node.js):
const {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} = require('@simplewebauthn/server');
const rpID = 'example.com';
const rpName = 'Your App Name';
const origin = 'https://example.com';
// Generate registration options
app.post('/api/auth/passkey/register-options', async (req, res) => {
const { username } = req.body;
const user = await db.users.findOne({ username });
if (!user) {
return res.status(400).json({ error: 'User not found' });
}
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: username,
userDisplayName: user.displayName,
attestationType: 'none',
authenticatorSelection: {
userVerification: 'required',
residentKey: 'preferred' // Enable discoverable credentials
},
timeout: 60000
});
// Store challenge for verification
await redis.setex(
`passkey-challenge:${user.id}`,
300, // 5 minutes
options.challenge
);
res.json(options);
});
// Verify registration
app.post('/api/auth/passkey/register', async (req, res) => {
const { credentialData, userId } = req.body;
const expectedChallenge = await redis.get(`passkey-challenge:${userId}`);
if (!expectedChallenge) {
return res.status(400).json({ error: 'Challenge expired' });
}
try {
const verification = await verifyRegistrationResponse({
response: credentialData,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID
});
if (!verification.verified) {
return res.status(400).json({ error: 'Verification failed' });
}
// Store credential
await db.passkeys.create({
userId,
credentialID: verification.registrationInfo.credentialID,
credentialPublicKey: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter,
transports: credentialData.response.transports
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Generate authentication options
app.post('/api/auth/passkey/auth-options', async (req, res) => {
const options = await generateAuthenticationOptions({
rpID,
userVerification: 'required',
timeout: 60000
});
// Store challenge
const challengeId = crypto.randomBytes(16).toString('hex');
await redis.setex(`auth-challenge:${challengeId}`, 300, options.challenge);
res.json({
...options,
challengeId
});
});
// Verify authentication
app.post('/api/auth/passkey/authenticate', async (req, res) => {
const { credentialData, challengeId } = req.body;
const expectedChallenge = await redis.get(`auth-challenge:${challengeId}`);
if (!expectedChallenge) {
return res.status(400).json({ error: 'Challenge expired' });
}
// Find passkey credential
const passkey = await db.passkeys.findOne({
credentialID: credentialData.id
});
if (!passkey) {
return res.status(400).json({ error: 'Unknown credential' });
}
try {
const verification = await verifyAuthenticationResponse({
response: credentialData,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: {
credentialID: passkey.credentialID,
credentialPublicKey: passkey.credentialPublicKey,
counter: passkey.counter
}
});
if (!verification.verified) {
return res.status(400).json({ error: 'Verification failed' });
}
// Update counter (prevents cloned authenticators)
await db.passkeys.update(passkey.id, {
counter: verification.authenticationInfo.newCounter
});
// Create session
const sessionToken = await createSession(passkey.userId);
res.json({
success: true,
token: sessionToken
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
For a complete step-by-step implementation guide with all edge cases covered, check out my FIDO2 authentication implementation guide.
Passkey Best Practices:
For more details on the future of passkeys, read my article on Passkeys: The Future of Passwordless Authentication.
OAuth 2.0 is the industry standard for authorization, and OpenID Connect (OIDC) adds an identity layer on top. Together, they power most modern authentication flows.
The Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the most secure OAuth 2.0 flow for web and mobile applications.
Flow Overview:
Implementation:
const crypto = require('crypto');
const express = require('express');
const axios = require('axios');
// OAuth configuration
const config = {
clientId: 'your-client-id',
clientSecret: 'your-client-secret', // Not used with PKCE for public clients
authorizationEndpoint: 'https://provider.com/oauth/authorize',
tokenEndpoint: 'https://provider.com/oauth/token',
redirectUri: 'https://yourapp.com/callback',
scope: 'openid profile email'
};
// Generate PKCE parameters
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Initiate OAuth flow
app.get('/auth/login', (req, res) => {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
// Store verifier and state in session (or encrypted cookie)
req.session.pkceVerifier = verifier;
req.session.oauthState = state;
// Build authorization URL
const authUrl = new URL(config.authorizationEndpoint);
authUrl.searchParams.append('client_id', config.clientId);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', config.redirectUri);
authUrl.searchParams.append('scope', config.scope);
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', challenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
res.redirect(authUrl.toString());
});
// Handle OAuth callback
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state parameter (CSRF protection)
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}
const verifier = req.session.pkceVerifier;
if (!verifier) {
return res.status(400).send('Missing PKCE verifier');
}
try {
// Exchange authorization code for tokens
const tokenResponse = await axios.post(config.tokenEndpoint, {
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: verifier
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const {
access_token,
refresh_token,
id_token,
expires_in
} = tokenResponse.data;
// Verify and decode ID token (OIDC)
const userInfo = await verifyIDToken(id_token);
// Create local session
const sessionToken = await createSession(userInfo.sub, {
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: Date.now() + (expires_in * 1000)
});
// Clear PKCE session data
delete req.session.pkceVerifier;
delete req.session.oauthState;
// Set session cookie and redirect
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
});
res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error);
res.status(500).send('Authentication failed');
}
});
// Verify ID token (simplified - use a JWT library in production)
async function verifyIDToken(idToken) {
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Get signing keys from provider's JWKS endpoint
const client = jwksClient({
jwksUri: 'https://provider.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
return new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
audience: config.clientId,
issuer: 'https://provider.com'
}, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}
// Refresh access token when expired
async function refreshAccessToken(refreshToken) {
try {
const response = await axios.post(config.tokenEndpoint, {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: config.clientId
});
return response.data;
} catch (error) {
throw new Error('Token refresh failed');
}
}
For a deep dive into OAuth flows and enterprise SSO patterns, read my comprehensive guide on SSO Deep Dive: SAML, OAuth & SCIM.
Never store tokens in localStorage or sessionStorage – they're vulnerable to XSS attacks.
Best practices:
// Secure token storage pattern
class TokenManager {
constructor() {
this.accessToken = null;
this.tokenExpiry = null;
}
setTokens(accessToken, expiresIn, refreshToken) {
// Store access token in memory
this.accessToken = accessToken;
this.tokenExpiry = Date.now() + (expiresIn * 1000);
// Store refresh token in httpOnly cookie (server-side)
// This happens on the server when setting the cookie
}
async getValidAccessToken() {
// Check if token is expired or about to expire (30 second buffer)
if (!this.accessToken || Date.now() >= (this.tokenExpiry - 30000)) {
// Refresh token
const newTokens = await this.refreshToken();
this.setTokens(
newTokens.access_token,
newTokens.expires_in,
newTokens.refresh_token
);
}
return this.accessToken;
}
async refreshToken() {
// Call backend endpoint that has access to httpOnly refresh token
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include' // Include httpOnly cookies
});
return response.json();
}
clearTokens() {
this.accessToken = null;
this.tokenExpiry = null;
// Also clear refresh token cookie on server
}
}
Proper session management is critical for maintaining security while providing a good user experience.
app.use(session({
name: 'sessionId', // Don't use default names like 'connect.sid'
secret: process.env.SESSION_SECRET, // Strong random secret
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Prevents JavaScript access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: '.yourapp.com' // Scope to your domain
},
store: new RedisStore({
client: redisClient,
prefix: 'sess:'
})
}));
async function validateSession(sessionId, requestContext) {
// Get session from Redis
const session = await redis.get(`sess:${sessionId}`);
if (!session) {
return { valid: false, reason: 'Session not found' };
}
const sessionData = JSON.parse(session);
// Check expiration
if (Date.now() > sessionData.expiresAt) {
await redis.del(`sess:${sessionId}`);
return { valid: false, reason: 'Session expired' };
}
// Validate request context (detect session hijacking)
const contextValid = validateSessionContext(sessionData, requestContext);
if (!contextValid) {
// Suspicious activity - invalidate session
await redis.del(`sess:${sessionId}`);
await logSecurityEvent('session_hijacking_attempt', sessionData.userId);
return { valid: false, reason: 'Context validation failed' };
}
// Update last activity (sliding expiration)
sessionData.lastActivity = Date.now();
await redis.setex(
`sess:${sessionId}`,
24 * 60 * 60, // 24 hours
JSON.stringify(sessionData)
);
return { valid: true, userId: sessionData.userId };
}
function validateSessionContext(sessionData, currentContext) {
// Compare IP addresses (allow some flexibility for mobile networks)
const sessionIP = sessionData.ipAddress;
const currentIP = currentContext.ipAddress;
// Check if IPs are in same /24 subnet
const sessionSubnet = sessionIP.split('.').slice(0, 3).join('.');
const currentSubnet = currentIP.split('.').slice(0, 3).join('.');
if (sessionSubnet !== currentSubnet) {
return false;
}
// Compare user agents (must match exactly)
if (sessionData.userAgent !== currentContext.userAgent) {
return false;
}
return true;
}
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// Generate CSRF token
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Protect state-changing operations
app.post('/api/user/delete', csrfProtection, async (req, res) => {
// CSRF token is automatically validated by middleware
const userId = req.session.userId;
await deleteUser(userId);
res.json({ success: true });
});
For B2B SaaS applications, enterprise SSO is often a must-have feature. SAML 2.0 remains the dominant protocol in enterprise environments.
const saml2 = require('saml2-js');
// Configure SAML Service Provider
const sp = new saml2.ServiceProvider({
entity_id: 'https://yourapp.com/saml/metadata',
private_key: fs.readFileSync('path/to/sp-key.pem').toString(),
certificate: fs.readFileSync('path/to/sp-cert.pem').toString(),
assert_endpoint: 'https://yourapp.com/saml/assert',
allow_unencrypted_assertion: false
});
// Configure Identity Provider (this would be per-tenant in production)
const idp = new saml2.IdentityProvider({
sso_login_url: 'https://idp.example.com/saml/login',
sso_logout_url: 'https://idp.example.com/saml/logout',
certificates: [fs.readFileSync('path/to/idp-cert.pem').toString()]
});
// Initiate SAML login
app.get('/saml/login', (req, res) => {
sp.create_login_request_url(idp, {}, (err, loginUrl, requestId) => {
if (err) {
return res.status(500).send('SAML login initialization failed');
}
// Store request ID for validation
req.session.samlRequestId = requestId;
res.redirect(loginUrl);
});
});
// Handle SAML assertion
app.post('/saml/assert', express.urlencoded({ extended: false }), (req, res) => {
const options = {
request_body: req.body,
allow_unencrypted_assertion: false
};
sp.post_assert(idp, options, async (err, samlResponse) => {
if (err) {
console.error('SAML assertion failed:', err);
return res.status(401).send('Authentication failed');
}
// Extract user attributes
const {
user: {
name_id: email,
attributes: {
firstName,
lastName,
groups
}
},
session_index: sessionIndex
} = samlResponse;
// Create or update user
const user = await getOrCreateUser(email, {
firstName,
lastName,
groups
});
// Create session
const sessionToken = await createSession(user.id, {
samlSessionIndex: sessionIndex,
authMethod: 'saml'
});
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
res.redirect('/dashboard');
});
});
// SAML metadata endpoint (for IdP configuration)
app.get('/saml/metadata', (req, res) => {
res.type('application/xml');
res.send(sp.create_metadata());
});
For enterprise identity patterns and why traditional approaches fail at scale, read my article on Enterprise Identity: Why SSO & RBAC Fail at Scale.
For SaaS applications serving multiple organizations:
class MultiTenantAuthenticator {
async authenticate(identifier, credentials) {
// Resolve tenant from identifier
const tenant = await this.resolveTenant(identifier);
if (!tenant || !tenant.active) {
throw new Error('Invalid or inactive tenant');
}
// Check authentication method for tenant
switch (tenant.authMethod) {
case 'saml':
return this.authenticateSAML(tenant, credentials);
case 'oidc':
return this.authenticateOIDC(tenant, credentials);
case 'password':
return this.authenticatePassword(tenant, credentials);
default:
throw new Error('Unsupported auth method');
}
}
async resolveTenant(identifier) {
// Support multiple tenant identification methods:
// 1. Subdomain (acme.yourapp.com)
// 2. Custom domain (app.acmecorp.com)
// 3. Email domain (@acmecorp.com)
if (identifier.includes('.yourapp.com')) {
const slug = identifier.split('.')[0];
return db.tenants.findOne({ slug });
}
if (identifier.includes('@')) {
const domain = identifier.split('@')[1];
return db.tenants.findOne({ emailDomains: domain });
}
return db.tenants.findOne({ customDomain: identifier });
}
generateTenantToken(userId, tenantId) {
const payload = {
sub: userId,
tenant: tenantId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
};
// Use tenant-specific signing key for isolation
const signingKey = this.getTenantSigningKey(tenantId);
return jwt.sign(payload, signingKey, { algorithm: 'RS256' });
}
}
Modern applications are built as distributed systems with multiple services. Here's how to handle authentication across microservices.
// API Gateway - Issue JWT for authenticated requests
app.use('/api', async (req, res, next) => {
const sessionToken = req.cookies.session;
const session = await validateSession(sessionToken);
if (!session.valid) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Generate JWT for internal services
const serviceJWT = jwt.sign({
sub: session.userId,
type: 'internal',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300 // 5 minutes
}, process.env.SERVICE_JWT_SECRET);
// Forward to service with JWT
req.headers['X-Service-Token'] = serviceJWT;
next();
});
// Microservice - Validate JWT
function validateServiceToken(req, res, next) {
const token = req.headers['x-service-token'];
if (!token) {
return res.status(401).json({ error: 'Missing service token' });
}
try {
const decoded = jwt.verify(token, process.env.SERVICE_JWT_SECRET);
if (decoded.type !== 'internal') {
return res.status(401).json({ error: 'Invalid token type' });
}
req.userId = decoded.sub;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Use in service routes
app.get('/api/orders', validateServiceToken, async (req, res) => {
const orders = await getOrders(req.userId);
res.json(orders);
});
For comprehensive API authentication patterns, check out my guide on Mastering API Authentication: 4 Methods.
For AI agents and automated services:
class M2MAuthenticator {
async issueServiceCredential(serviceAccountId) {
// Generate time-limited JWT for service account
const token = jwt.sign({
sub: serviceAccountId,
type: 'service_account',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) // 24 hours
}, process.env.SERVICE_ACCOUNT_KEY, {
algorithm: 'RS256'
});
// Track credential issuance
await db.credentials.create({
serviceAccountId,
tokenId: crypto.randomBytes(16).toString('hex'),
issuedAt: new Date(),
expiresAt: new Date(Date.now() + (24 * 60 * 60 * 1000))
});
return token;
}
async rotateCredential(oldToken) {
// Decode old token (don't verify - we're rotating)
const decoded = jwt.decode(oldToken);
// Issue new credential
const newToken = await this.issueServiceCredential(decoded.sub);
// Mark old credential as rotated (grace period: 1 hour)
await db.credentials.update({
serviceAccountId: decoded.sub,
tokenId: decoded.jti
}, {
rotated: true,
graceExpiresAt: new Date(Date.now() + (60 * 60 * 1000))
});
return newToken;
}
}
For AI agent authentication specifically, see my guide on AI Agent Authentication.
Before launching your authentication system, ensure you've addressed these critical security points:
Authentication Basics:
Session Management:
Token Security:
OAuth/OIDC:
API Security:
Compliance:
Monitoring:
Throughout this guide, I've shared implementation patterns proven at billion-user scale. But authentication is complex, and every application has unique requirements.
That's why I've build a comprehensive authentication-implementation skill to the AI Agents like Claude code, Google Antigravity, etc.
This skill provides Claude, Gemini, etc. LLMs with deep expertise in:
GitHub – guptadeepak/auth-implementation-skill: AI agent skill for Authentication Implementation, using this skill your IDE can setup secure auth for your app
AI agent skill for Authentication Implementation, using this skill your IDE can setup secure auth for your app – GitHub – guptadeepak/auth-implementation-skill: AI agent skill for Authentication I…
GitHubguptadeepak
How to use it:
Simply mention your authentication requirements to Claude:
"I need to implement OAuth 2.0 authentication for my React app with a Node.js backend. The app will have both consumer users and enterprise customers requiring SSO."
Claude will leverage this skill to provide tailored implementation guidance, security best practices, and production-ready code specific to your stack.
Authentication is just one piece of the broader identity management puzzle. Modern applications need to consider:
For a comprehensive view of how these pieces fit together, read my guide on Understanding the Complete Identity Management Ecosystem.
Authentication is no longer just a technical requirement—it's a competitive differentiator. Companies that implement modern, secure authentication:
The patterns in this guide are based on real-world experience scaling identity systems to serve over 1 billion users. They're production-tested, security-hardened, and designed for the challenges you'll face as you grow.
Throughout this guide, I've linked to detailed implementation guides for specific protocols and patterns:
For ongoing updates on authentication trends, security best practices, and identity management, subscribe to my newsletter or follow me on LinkedIn and X.
*** This is a Security Bloggers Network syndicated blog from Deepak Gupta | AI & Cybersecurity Innovation Leader | Founder's Journey from Code to Scale authored by Deepak Gupta - Tech Entrepreneur, Cybersecurity Author. Read the original post at: https://guptadeepak.com/the-complete-guide-to-authentication-implementation-for-modern-applications/