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 };
}
}
}