Security
Last updated
Last updated
Security is paramount when implementing SSO. This guide covers essential security practices to protect your users and applications.
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
// 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) => {
Single Page Applications (SPAs)
Mobile applications
Any client that cannot securely store secrets
Next: Learn about Token Management best practices
// NEVER use HTTP in production
const redirectURI = "http://myapp.com/callback"; // Vulnerable!// 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
}
});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;
}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...
}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
};// 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);
}// 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');
}
}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);
});
});
}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 };
}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;
}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
});
}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
})
}));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();
});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 });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);📖 Comprehensive PKCE Guide: For complete implementation examples for SPAs and native apps, see the PKCE Implementation Guide.