@citrineos/util
Version:
The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.
202 lines • 9.04 kB
JavaScript
"use strict";
/*
* // Copyright Contributors to the CitrineOS Project
* //
* // SPDX-License-Identifier: Apache 2.0
*
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OIDCAuthProvider = void 0;
const tslog_1 = require("tslog");
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const jwks_rsa_1 = __importDefault(require("jwks-rsa"));
const base_1 = require("@citrineos/base");
const crypto_1 = require("crypto");
const RbacRulesLoader_1 = require("../rbac/RbacRulesLoader");
/**
* OIDC authentication provider implementation
*/
class OIDCAuthProvider {
/**
* Creates a new Keycloak authentication provider
*
* @param config OIDC configuration
* @param logger Optional logger instance
*/
constructor(config, logger) {
this._defaultTenantId = '1'; //TODO get default from config
this._config = Object.assign({ cacheTime: 60 * 60 * 1000, rateLimit: true }, config);
this._logger = logger
? logger.getSubLogger({ name: this.constructor.name })
: new tslog_1.Logger({ name: this.constructor.name });
this._logger.info('OIDC auth provider config', this._config);
// Create the JWKS client
this._jkwsClient = (0, jwks_rsa_1.default)({
jwksUri: this._config.jwksUri,
cache: true,
cacheMaxAge: this._config.cacheTime,
rateLimit: this._config.rateLimit,
jwksRequestsPerMinute: 5, // Limit requests to JWKS endpoint
});
this._rulesLoader = new RbacRulesLoader_1.RbacRulesLoader('rbac-rules.json', this._logger);
this._logger.info(`OIDC auth provider setup with jwksUri: ${this._config.jwksUri}`);
}
extractToken(request) {
return __awaiter(this, void 0, void 0, function* () {
// Extract the Authorization header
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
this._logger.warn('No Bearer token found in request headers');
return null;
}
// Return the token without the "Bearer " prefix
const token = authHeader.slice(7).trim();
this._logger.debug('Extracted token from request:', token);
return token;
});
}
/**
* Authenticates a JWT token from and OIDC provider
*
* @param token JWT token to authenticate
* @returns Authentication result with user info if successful
*/
authenticateToken(token) {
return __awaiter(this, void 0, void 0, function* () {
try {
const decoded = jsonwebtoken_1.default.decode(token, { complete: true });
if (!decoded || typeof decoded !== 'object' || !decoded.header || !decoded.header.kid) {
throw new Error('Invalid token format');
}
const publicKey = yield this.fetchPublicKey(decoded.header.kid);
// Verify the token with the public key
const payload = jsonwebtoken_1.default.verify(token, (0, crypto_1.createPublicKey)(publicKey));
// Extract user info from the decoded token
const user = {
id: payload.sub,
name: payload.preferred_username || payload.name || payload.sub,
email: payload.email || '',
roles: this.extractRoles(payload),
tenantId: payload.tenant_id || this._defaultTenantId,
metadata: {
firstName: payload.given_name,
lastName: payload.family_name,
fullName: payload.name,
emailVerified: payload.email_verified,
locale: payload.locale || 'en-US',
},
};
return base_1.ApiAuthenticationResult.success(user);
}
catch (error) {
this._logger.error('Token authentication failed:', error);
return base_1.ApiAuthenticationResult.failure(error instanceof Error ? error.message : 'Invalid token');
}
});
}
/**
* Authorizes a user for a specific request
* This implementation checks if the user has the required permissions
* for the requested URL and method
*
* @param user User information
* @param request Fastify request
* @returns Authorization result
*/
authorizeUser(user, request) {
return __awaiter(this, void 0, void 0, function* () {
try {
// Get the requested resource and method
const url = request.url;
const method = request.method;
const tenantId = request.query.tenantId || this._defaultTenantId;
const requiredRoles = this._rulesLoader.getRequiredRoles(tenantId, url, method);
//If no role is found for the requested resource and tenant, decline access
if (!requiredRoles || requiredRoles.length === 0) {
return base_1.ApiAuthorizationResult.failure(`Tenant does not have access to this resource ${url}`);
}
if (this.userHasRequiredRole(user, requiredRoles)) {
return base_1.ApiAuthorizationResult.success();
}
return base_1.ApiAuthorizationResult.failure(`Missing required roles. Need one of: ${requiredRoles.join(', ')} for tenant ${tenantId}`);
}
catch (error) {
this._logger.error('Authorization error:', error);
return base_1.ApiAuthorizationResult.failure(`Authorization error: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
/**
* Fetches the public key from OIDC provider
* @param {string} kid Key ID from the JWT header
* @returns {Promise<string>} Public key as a string
* @private
*/
fetchPublicKey(kid) {
return __awaiter(this, void 0, void 0, function* () {
try {
return new Promise((resolve, reject) => {
this._jkwsClient.getSigningKey(kid, (err, key) => {
if (err) {
this._logger.error(`Error fetching signing key for kid: ${kid}`, err);
return reject(err);
}
if (!key) {
const error = new Error(`No signing key found for kid: ${kid}`);
this._logger.error(error.message);
return reject(error);
}
try {
// Get the public key
const signingKey = key.getPublicKey();
resolve(signingKey);
}
catch (keyError) {
this._logger.error('Error extracting public key:', keyError);
reject(keyError);
}
});
});
}
catch (error) {
this._logger.error('Failed to fetch public key:', error);
throw error;
}
});
}
/**
* Extracts roles from a decoded JWT token
*
* @param decoded The decoded JWT token
* @returns Array of role strings
* @private
*/
extractRoles(decoded) {
//Customize here to match your token structure
return decoded.roles || [];
}
/**
* Check if a user has any of the required roles for a specific tenant
*
* @param user User with roles
* @param requiredRoles Array of role names (without tenant prefix)
* @returns True if user has any of the required roles
*/
userHasRequiredRole(user, requiredRoles) {
return user.roles.some((userRole) => requiredRoles.includes(userRole));
}
}
exports.OIDCAuthProvider = OIDCAuthProvider;
//# sourceMappingURL=OIDCAuthProvider.js.map