Security Standards Implementation

Security Standards Implementation

Implementing security standards like OAuth 2.0 and OpenID Connect requires careful attention to specification details. OAuth 2.0 Security Best Current Practice (BCP) provides crucial guidance beyond the base specification, mandating PKCE for public clients and recommending it for confidential clients. The standard prohibits the implicit flow due to security concerns and requires exact redirect URI matching to prevent authorization code injection attacks.

// Node.js implementation of OAuth 2.0 with security best practices
const crypto = require('crypto');
const jose = require('jose');

class OAuth2SecureImplementation {
    constructor(config) {
        this.config = config;
        this.clientStore = new Map();
        this.authorizationCodes = new Map();
        this.refreshTokens = new Map();
        
        // Initialize JWKS for token signing
        this.initializeJWKS();
    }
    
    async initializeJWKS() {
        // Generate or load signing keys
        this.signingKey = await jose.JWK.generate('RSA', 2048, {
            alg: 'RS256',
            use: 'sig',
            kid: crypto.randomBytes(16).toString('hex')
        });
        
        // Public key for verification
        this.publicKeySet = new jose.JWKS.KeyStore();
        this.publicKeySet.add(this.signingKey);
    }
    
    // Client registration with security validations
    async registerClient(clientMetadata) {
        // Validate client metadata according to RFC 7591
        this.validateClientMetadata(clientMetadata);
        
        const clientId = crypto.randomBytes(16).toString('hex');
        const clientSecret = crypto.randomBytes(32).toString('base64url');
        
        // Store client with security constraints
        const client = {
            client_id: clientId,
            client_secret_hash: await this.hashClientSecret(clientSecret),
            redirect_uris: clientMetadata.redirect_uris,
            grant_types: clientMetadata.grant_types || ['authorization_code'],
            response_types: clientMetadata.response_types || ['code'],
            token_endpoint_auth_method: clientMetadata.token_endpoint_auth_method || 'client_secret_basic',
            application_type: clientMetadata.application_type || 'web',
            require_pkce: clientMetadata.application_type === 'native' || this.config.requirePKCE,
            allowed_scopes: this.validateScopes(clientMetadata.scope),
            jwks_uri: clientMetadata.jwks_uri,
            created_at: new Date()
        };
        
        this.clientStore.set(clientId, client);
        
        return {
            client_id: clientId,
            client_secret: clientSecret,
            client_secret_expires_at: 0, // Never expires
            ...this.filterClientResponse(client)
        };
    }
    
    // Authorization endpoint with security checks
    async authorize(params) {
        // Validate all parameters
        const validation = this.validateAuthorizationRequest(params);
        if (validation.error) {
            return this.createErrorResponse(validation.error, params.state);
        }
        
        const client = this.clientStore.get(params.client_id);
        
        // Enforce PKCE for public clients
        if (this.isPublicClient(client) && !params.code_challenge) {
            return this.createErrorResponse('invalid_request', params.state,
                'PKCE is required for public clients');
        }
        
        // Validate redirect URI with exact matching
        if (!this.validateRedirectUri(params.redirect_uri, client.redirect_uris)) {
            return this.createErrorResponse('invalid_request', null,
                'Invalid redirect_uri');
        }
        
        // Generate authorization code with metadata
        const code = crypto.randomBytes(32).toString('base64url');
        const codeData = {
            client_id: params.client_id,
            redirect_uri: params.redirect_uri,
            scope: params.scope,
            user_id: params.user_id, // From authentication
            code_challenge: params.code_challenge,
            code_challenge_method: params.code_challenge_method || 'plain',
            nonce: params.nonce,
            created_at: Date.now(),
            expires_at: Date.now() + 600000 // 10 minutes
        };
        
        this.authorizationCodes.set(code, codeData);
        
        // Build response
        const responseParams = new URLSearchParams({
            code: code,
            state: params.state
        });
        
        return {
            redirect: `${params.redirect_uri}?${responseParams}`
        };
    }
    
    // Token endpoint with comprehensive security
    async token(params) {
        if (params.grant_type === 'authorization_code') {
            return this.handleAuthorizationCodeGrant(params);
        } else if (params.grant_type === 'refresh_token') {
            return this.handleRefreshTokenGrant(params);
        } else if (params.grant_type === 'client_credentials') {
            return this.handleClientCredentialsGrant(params);
        }
        
        return this.createTokenErrorResponse('unsupported_grant_type');
    }
    
    async handleAuthorizationCodeGrant(params) {
        // Authenticate client
        const client = await this.authenticateClient(params);
        if (!client) {
            return this.createTokenErrorResponse('invalid_client');
        }
        
        // Validate authorization code
        const codeData = this.authorizationCodes.get(params.code);
        if (!codeData) {
            return this.createTokenErrorResponse('invalid_grant');
        }
        
        // Check code expiration
        if (Date.now() > codeData.expires_at) {
            this.authorizationCodes.delete(params.code);
            return this.createTokenErrorResponse('invalid_grant');
        }
        
        // Validate code binding
        if (codeData.client_id !== client.client_id ||
            codeData.redirect_uri !== params.redirect_uri) {
            this.authorizationCodes.delete(params.code);
            return this.createTokenErrorResponse('invalid_grant');
        }
        
        // Verify PKCE
        if (codeData.code_challenge) {
            if (!params.code_verifier) {
                return this.createTokenErrorResponse('invalid_request',
                    'code_verifier required');
            }
            
            const verifierValid = this.verifyCodeChallenge(
                params.code_verifier,
                codeData.code_challenge,
                codeData.code_challenge_method
            );
            
            if (!verifierValid) {
                this.authorizationCodes.delete(params.code);
                return this.createTokenErrorResponse('invalid_grant');
            }
        }
        
        // Delete used code (one-time use)
        this.authorizationCodes.delete(params.code);
        
        // Generate tokens
        const tokens = await this.generateTokens(
            client,
            codeData.user_id,
            codeData.scope,
            codeData.nonce
        );
        
        return tokens;
    }
    
    async generateTokens(client, userId, scope, nonce) {
        const now = Math.floor(Date.now() / 1000);
        
        // Access token claims
        const accessTokenClaims = {
            iss: this.config.issuer,
            sub: userId,
            aud: client.client_id,
            exp: now + 3600, // 1 hour
            iat: now,
            scope: scope,
            client_id: client.client_id,
            jti: crypto.randomBytes(16).toString('hex')
        };
        
        // Sign access token
        const accessToken = await new jose.JWT.Sign(
            Buffer.from(JSON.stringify(accessTokenClaims))
        )
            .recipient(this.signingKey)
            .sign('compact');
        
        // Generate refresh token with rotation
        const refreshToken = crypto.randomBytes(32).toString('base64url');
        const refreshTokenData = {
            client_id: client.client_id,
            user_id: userId,
            scope: scope,
            created_at: now,
            expires_at: now + 2592000, // 30 days
            family_id: crypto.randomBytes(16).toString('hex')
        };
        
        this.refreshTokens.set(refreshToken, refreshTokenData);
        
        const response = {
            access_token: accessToken,
            token_type: 'Bearer',
            expires_in: 3600,
            refresh_token: refreshToken,
            scope: scope
        };
        
        // Include ID token for OpenID Connect
        if (scope.includes('openid')) {
            const idTokenClaims = {
                iss: this.config.issuer,
                sub: userId,
                aud: client.client_id,
                exp: now + 3600,
                iat: now,
                nonce: nonce,
                auth_time: now,
                acr: 'urn:mace:incommon:iap:silver'
            };
            
            const idToken = await new jose.JWT.Sign(
                Buffer.from(JSON.stringify(idTokenClaims))
            )
                .recipient(this.signingKey)
                .sign('compact');
            
            response.id_token = idToken;
        }
        
        return response;
    }
    
    // Token introspection endpoint
    async introspect(token, tokenTypeHint) {
        try {
            // Try to decode as JWT
            const decoded = await jose.JWT.verify(token, this.publicKeySet);
            
            // Check if token is revoked
            if (await this.isTokenRevoked(decoded.jti)) {
                return { active: false };
            }
            
            return {
                active: decoded.exp > Math.floor(Date.now() / 1000),
                scope: decoded.scope,
                client_id: decoded.client_id,
                username: decoded.sub,
                exp: decoded.exp,
                iat: decoded.iat,
                sub: decoded.sub,
                aud: decoded.aud,
                iss: decoded.iss,
                jti: decoded.jti
            };
        } catch (error) {
            // Check if it's a refresh token
            const refreshData = this.refreshTokens.get(token);
            if (refreshData) {
                return {
                    active: refreshData.expires_at > Math.floor(Date.now() / 1000),
                    scope: refreshData.scope,
                    client_id: refreshData.client_id,
                    username: refreshData.user_id,
                    exp: refreshData.expires_at
                };
            }
            
            return { active: false };
        }
    }
}