Security

Security is paramount when implementing SSO. This guide covers essential security practices to protect your users and applications.

Security Fundamentals

Core Principles

  1. Defense in Depth: Multiple layers of security

  2. Least Privilege: Grant minimum necessary access

  3. Zero Trust: Verify everything, trust nothing

  4. 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