Security
Security is paramount when implementing SSO. This guide covers essential security practices to protect your users and applications.
Security Fundamentals
Core Principles
Defense in Depth: Multiple layers of security
Least Privilege: Grant minimum necessary access
Zero Trust: Verify everything, trust nothing
Secure by Default: Safe configurations out of the box
Transport Security
Always Use HTTPS
✅ Correct Implementation
// Production configuration
const redirectURI = "https://myapp.com/callback";
const authURL = "https://account.oten.com/v1/oauth/authorize";
// Enforce HTTPS in your application
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});❌ Wrong Implementation
// NEVER use HTTP in production
const redirectURI = "http://myapp.com/callback"; // Vulnerable!Certificate Validation
// Verify SSL certificates
const https = require('https');
const agent = new https.Agent({
rejectUnauthorized: true, // Always verify certificates
checkServerIdentity: (host, cert) => {
// Additional certificate validation if needed
return undefined; // No error = valid
}
});🛡️ CSRF Protection
State Parameter Implementation
Generate Secure State
const crypto = require('crypto');
function generateState() {
// Generate cryptographically secure random state
return crypto.randomBytes(32).toString('hex');
}
function createAuthURL() {
const state = generateState();
// Store state in session for later verification
req.session.oauthState = state;
const authURL = buildAuthorizationURL({
client_id: clientId,
redirect_uri: redirectURI,
response_type: 'code',
scope: 'openid profile email',
state: state
});
return authURL;
}Validate State
function validateCallback(req, res) {
const receivedState = req.query.state;
const storedState = req.session.oauthState;
// Clear stored state
delete req.session.oauthState;
if (!receivedState || receivedState !== storedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// Continue with token exchange...
}🔐 PKCE for Public Clients
📖 Comprehensive PKCE Guide: For complete implementation examples for SPAs and native apps, see the PKCE Implementation Guide.
When to Use PKCE
Single Page Applications (SPAs)
Mobile applications
Any client that cannot securely store secrets
PKCE Implementation
const crypto = require('crypto');
function generatePKCE() {
// Generate code verifier (43-128 characters)
const codeVerifier = crypto.randomBytes(96).toString('base64url');
// Generate code challenge
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: 'S256'
};
}
// In authorization flow
const pkce = generatePKCE();
// Store code verifier securely (session storage for SPAs)
sessionStorage.setItem('code_verifier', pkce.codeVerifier);
// Include in authorization URL
const authURL = buildAuthorizationURL({
// ... other parameters
code_challenge: pkce.codeChallenge,
code_challenge_method: pkce.codeChallengeMethod
});
// In token exchange
const tokenRequest = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: redirectURI,
client_id: clientId,
code_verifier: sessionStorage.getItem('code_verifier')
// Note: No client_secret for public clients
};🎫 Token Security
Secure Token Storage
Server-Side Applications
// Store tokens encrypted in database
const crypto = require('crypto');
function encryptToken(token, secretKey) {
const cipher = crypto.createCipher('aes-256-cbc', secretKey);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function decryptToken(encryptedToken, secretKey) {
const decipher = crypto.createDecipher('aes-256-cbc', secretKey);
let decrypted = decipher.update(encryptedToken, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Save user tokens
async function saveUserTokens(userId, tokens) {
const encryptedTokens = {
accessToken: encryptToken(tokens.access_token, process.env.TOKEN_ENCRYPTION_KEY),
refreshToken: encryptToken(tokens.refresh_token, process.env.TOKEN_ENCRYPTION_KEY),
expiresAt: new Date(Date.now() + tokens.expires_in * 1000)
};
await database.saveUserTokens(userId, encryptedTokens);
}Client-Side Applications (SPAs)
// Use secure storage mechanisms
class SecureTokenStorage {
constructor() {
this.storage = window.sessionStorage; // More secure than localStorage
}
setTokens(tokens) {
// Store with expiration
const tokenData = {
...tokens,
storedAt: Date.now()
};
this.storage.setItem('auth_tokens', JSON.stringify(tokenData));
}
getTokens() {
const stored = this.storage.getItem('auth_tokens');
if (!stored) return null;
const tokenData = JSON.parse(stored);
// Check if tokens are expired
const now = Date.now();
const expiresAt = tokenData.storedAt + (tokenData.expires_in * 1000);
if (now >= expiresAt) {
this.clearTokens();
return null;
}
return tokenData;
}
clearTokens() {
this.storage.removeItem('auth_tokens');
}
}Token Validation
ID Token Validation
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Create JWKS client
const client = jwksClient({
jwksUri: 'https://account.oten.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 86400000, // 24 hours
rateLimit: true,
jwksRequestsPerMinute: 5
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
return;
}
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
async function validateIDToken(idToken) {
return new Promise((resolve, reject) => {
jwt.verify(idToken, getKey, {
issuer: 'https://account.oten.com',
audience: process.env.CLIENT_ID,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
reject(new Error(`Invalid ID token: ${err.message}`));
return;
}
// Additional validations
const now = Math.floor(Date.now() / 1000);
if (decoded.exp <= now) {
reject(new Error('ID token has expired'));
return;
}
if (decoded.iat > now + 300) { // Allow 5 minutes clock skew
reject(new Error('ID token issued in the future'));
return;
}
resolve(decoded);
});
});
}🔍 Input Validation
Validate All OAuth Parameters
function validateOAuthCallback(req) {
const { code, state, error, error_description } = req.query;
// Check for OAuth errors
if (error) {
const errorMsg = error_description || error;
throw new Error(`OAuth error: ${errorMsg}`);
}
// Validate authorization code
if (!code || typeof code !== 'string') {
throw new Error('Missing or invalid authorization code');
}
if (code.length < 10 || code.length > 512) {
throw new Error('Authorization code length invalid');
}
// Validate state parameter
if (!state || typeof state !== 'string') {
throw new Error('Missing or invalid state parameter');
}
if (!/^[a-zA-Z0-9_-]+$/.test(state)) {
throw new Error('State parameter contains invalid characters');
}
return { code, state };
}Sanitize User Data
function sanitizeUserClaims(claims) {
const allowedClaims = [
'sub', 'email', 'name', 'given_name',
'family_name', 'picture', 'locale'
];
const sanitized = {};
for (const claim of allowedClaims) {
if (claims[claim] && typeof claims[claim] === 'string') {
// Basic sanitization
sanitized[claim] = claims[claim]
.trim()
.substring(0, 255) // Limit length
.replace(/[<>]/g, ''); // Remove potential XSS characters
}
}
return sanitized;
}Error Handling Security
Don't Leak Sensitive Information
function handleAuthError(error, req, res) {
// Log detailed error for debugging
console.error('Auth error details:', {
error: error.message,
stack: error.stack,
userAgent: req.headers['user-agent'],
ip: req.ip,
timestamp: new Date().toISOString()
});
// Return generic error to user
let userMessage = 'Authentication failed. Please try again.';
let statusCode = 400;
// Map specific errors to user-friendly messages
if (error.message.includes('access_denied')) {
userMessage = 'Access was denied. Please contact your administrator.';
} else if (error.message.includes('invalid_grant')) {
userMessage = 'Your session has expired. Please log in again.';
statusCode = 401;
} else if (error.message.includes('server_error')) {
userMessage = 'Service temporarily unavailable. Please try again later.';
statusCode = 503;
}
res.status(statusCode).json({
error: 'authentication_failed',
message: userMessage
});
}🔐 Session Security
Secure Session Configuration
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(session({
secret: process.env.SESSION_SECRET, // Strong, random secret
name: 'sessionId', // Don't use default name
resave: false,
saveUninitialized: false,
rolling: true, // Reset expiration on activity
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevent XSS
maxAge: 30 * 60 * 1000, // 30 minutes
sameSite: 'lax' // CSRF protection
},
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
touchAfter: 24 * 3600 // Lazy session update
})
}));Content Security Policy
CSP for OAuth Applications
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Minimize unsafe-inline
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://account.oten.com",
"frame-ancestors 'none'", // Prevent clickjacking
"base-uri 'self'",
"form-action 'self' https://account.oten.com"
].join('; '));
next();
});Security Monitoring
Monitor Authentication Events
function logSecurityEvent(event, details) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event,
severity: getSeverity(event),
details: details,
source: 'oauth-service'
};
// Send to security monitoring system
securityLogger.log(logEntry);
// Alert on suspicious patterns
if (logEntry.severity === 'HIGH') {
alertingService.sendAlert(logEntry);
}
}
// Usage examples
logSecurityEvent('login_success', { userId, ip, userAgent });
logSecurityEvent('login_failure', { username, ip, reason });
logSecurityEvent('token_refresh', { userId, ip });
logSecurityEvent('suspicious_activity', { userId, ip, details });Rate Limiting
const rateLimit = require('express-rate-limit');
// Rate limit for login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
// Rate limit by IP and username combination
return `${req.ip}:${req.body.username || 'unknown'}`;
}
});
app.post('/auth/callback', loginLimiter, handleCallback);Next: Learn about Token Management best practices
Last updated