# 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

```javascript
// 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

```javascript
// NEVER use HTTP in production
const redirectURI = "http://myapp.com/callback"; // Vulnerable!
```

### Certificate Validation

```javascript
// 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

```javascript
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

```javascript
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](https://gitlab.silvertiger.tech/documents/idp/-/blob/main/developer-guide/pkce-implementation-guide.md).

### When to Use PKCE

* **Single Page Applications (SPAs)**
* **Mobile applications**
* **Any client that cannot securely store secrets**

### PKCE Implementation

```javascript
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

```javascript
// 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)

```javascript
// 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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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](broken://pages/qthHz21llFsoIuOdHVd9) best practices


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://oten.gitbook.io/identity-support/integration/prerequisites/best-practice/security.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
