@voidkey/broker-core
Version:
Core credential minting logic for the voidkey zero-trust credential broker
188 lines • 9.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AWSProvider = void 0;
const base_1 = require("./base");
class AWSProvider extends base_1.AccessProvider {
constructor(config) {
super(config);
this.config = config;
}
getName() {
return this.config.name;
}
getType() {
return 'aws-sts';
}
async mintCredential(request) {
this.validateKeyConfig(request.keyConfig);
// Broker OIDC token validation is mandatory
if (!request.brokerToken) {
throw new Error('Broker OIDC token is required for AWS credential minting');
}
await this.validateBrokerToken(request.brokerToken);
const keyConfig = request.keyConfig;
const duration = keyConfig.duration || this.getDefaultDuration();
console.log(`🔄 AWS Provider: Minting credentials for key "${request.keyName}" (subject: ${request.subject})`);
console.log(`📝 AWS Provider: Role ARN: ${keyConfig.roleArn}`);
try {
// Use AWS STS AssumeRoleWithWebIdentity to get temporary credentials using OIDC token
const stsResult = await this.assumeRoleWithWebIdentity(request.brokerToken, keyConfig.roleArn, keyConfig.roleSessionName || `voidkey-${request.subject}-${Date.now()}`, keyConfig.policy, duration);
console.log(`✅ AWS Provider: Got STS credentials for role ${keyConfig.roleArn}`);
// Map the STS response to the configured output environment variables
const credentials = {};
for (const [envVar, fieldPath] of Object.entries(keyConfig.outputs)) {
switch (fieldPath) {
case 'AccessKeyId':
credentials[envVar] = stsResult.AccessKeyId;
break;
case 'SecretAccessKey':
credentials[envVar] = stsResult.SecretAccessKey;
break;
case 'SessionToken':
credentials[envVar] = stsResult.SessionToken || '';
break;
case 'Expiration':
credentials[envVar] = stsResult.Expiration || new Date(Date.now() + (duration * 1000)).toISOString();
break;
default:
console.warn(`⚠️ AWS Provider: Unknown field path "${fieldPath}" for env var "${envVar}"`);
}
}
return {
credentials,
expiresAt: stsResult.Expiration || new Date(Date.now() + (duration * 1000)).toISOString(),
metadata: {
provider: this.getName(),
keyName: request.keyName,
roleArn: keyConfig.roleArn,
roleSessionName: keyConfig.roleSessionName
}
};
}
catch (error) {
console.error(`❌ AWS Provider: Failed to mint credentials for key "${request.keyName}":`, error);
throw new Error(`AWS credential minting failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async healthCheck() {
try {
// TODO: Implement AWS STS health check
// Could call GetCallerIdentity to verify broker credentials work
console.log('AWS health check not yet implemented');
return true; // Placeholder
}
catch (error) {
console.error('AWS health check failed:', error);
return false;
}
}
async assumeRoleWithWebIdentity(brokerToken, roleArn, roleSessionName, policy, duration) {
try {
console.log(`🔧 AWS Provider: Assuming role via AssumeRoleWithWebIdentity`);
const actualDuration = duration || this.getDefaultDuration();
// AWS STS endpoint format
const stsEndpoint = this.config.endpoint;
// Prepare the STS request parameters
const params = new URLSearchParams({
'Action': 'AssumeRoleWithWebIdentity',
'Version': '2011-06-15',
'RoleArn': roleArn,
'RoleSessionName': roleSessionName,
'DurationSeconds': actualDuration.toString(),
'WebIdentityToken': brokerToken
});
// Add optional policy
if (policy) {
params.append('Policy', policy);
}
// Add role ARN from config if available
if (this.config.roleArn) {
params.append('RoleArn', this.config.roleArn);
}
const fetch = require('node-fetch');
const response = await fetch(`${stsEndpoint}?${params.toString()}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ AWS STS error: ${response.status} - ${errorText}`);
throw new Error(`AWS STS request failed: ${response.status} ${response.statusText}`);
}
const responseText = await response.text();
console.log(`✅ AWS Provider: Received STS response`);
// Parse the XML response
const credentials = await this.parseSTSResponse(responseText);
return credentials;
}
catch (error) {
console.error('Failed to assume AWS role with web identity:', error);
throw new Error(`AWS role assumption failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async parseSTSResponse(responseText) {
// Parse the XML response from AWS STS
// AWS STS returns XML similar to MinIO
// Simple XML parsing - in production, use a proper XML parser
const accessKeyMatch = responseText.match(/<AccessKeyId>(.*?)<\/AccessKeyId>/);
const secretKeyMatch = responseText.match(/<SecretAccessKey>(.*?)<\/SecretAccessKey>/);
const sessionTokenMatch = responseText.match(/<SessionToken>(.*?)<\/SessionToken>/);
const expirationMatch = responseText.match(/<Expiration>(.*?)<\/Expiration>/);
if (!accessKeyMatch || !secretKeyMatch) {
throw new Error('Invalid AWS STS response: missing credentials');
}
return {
AccessKeyId: accessKeyMatch[1],
SecretAccessKey: secretKeyMatch[1],
SessionToken: sessionTokenMatch ? sessionTokenMatch[1] : undefined,
Expiration: expirationMatch ? expirationMatch[1] : undefined
};
}
async validateBrokerToken(token) {
try {
console.log('🔐 AWS Provider: Validating broker OIDC token');
const jwt = require('jsonwebtoken');
// Decode token to get header and payload
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid JWT token format');
}
const payload = decoded.payload;
// Validate basic JWT structure
if (!payload.iss || !payload.aud || !payload.exp) {
throw new Error('JWT missing required claims (iss, aud, exp)');
}
// Check expiration
if (Date.now() >= payload.exp * 1000) {
throw new Error('Broker token has expired');
}
// Validate issuer (required)
if (payload.iss !== this.config.brokerAuth.expectedIssuer) {
throw new Error(`Invalid issuer: expected ${this.config.brokerAuth.expectedIssuer}, got ${payload.iss}`);
}
// Validate audience (required)
const tokenAudience = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
if (!tokenAudience.includes(this.config.brokerAuth.expectedAudience)) {
throw new Error(`Invalid audience: expected ${this.config.brokerAuth.expectedAudience}, got ${tokenAudience.join(', ')}`);
}
// TODO: Validate signature against JWKS if configured
// For now, we'll do basic validation and trust the broker
if (this.config.brokerAuth.jwksUri) {
console.log('🔍 AWS Provider: JWKS validation not yet implemented - trusting broker token');
// In production, implement full JWKS signature validation here
}
console.log('✅ AWS Provider: Broker token validated successfully');
console.log(` Subject: ${payload.sub || 'not provided'}`);
console.log(` Issuer: ${payload.iss}`);
console.log(` Audience: ${Array.isArray(payload.aud) ? payload.aud.join(', ') : payload.aud}`);
}
catch (error) {
console.error('❌ AWS Provider: Broker token validation failed:', error);
throw new Error(`Broker authentication failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
exports.AWSProvider = AWSProvider;
//# sourceMappingURL=aws.js.map