SAP-BTP-Spielwiese/app1/node_modules/@sap/xssec/lib/validator.js
Markus Rettig 775ac7b58c completed step 3 from the tutorial
you must login with an BTP account in order to see the app
2024-02-08 16:13:36 +01:00

523 lines
No EOL
20 KiB
JavaScript

'use strict';
const util = require('util');
const debug = require('debug');
const debugTrace = debug('xssec:validators');
debugTrace.log = console.log.bind(console);
const debugError = debug('xssec:validators');
debugError.log = console.error.bind(console);
const url = require('url');
const TokenInfo = require('./tokeninfo');
const TokenExchanger = require('./tokenexchanger');
const ValidationResults = require("./validator/ValidationResults");
const X5tValidator = require("./validator/X5tValidator");
const JwksManager = require('./jwks/JwksManager');
const JwksReplica = require('./jwks/JwksReplica');
let jwksManager;
const DOT = ".";
function JwtAudienceValidator(clientId) {
var clientIds = [];
var foreignMode = false;
this.configureTrustedClientId = function (clientId) {
if (clientId) {
clientIds.push(clientId);
}
debugTrace("configured JwtAudienceValidator with clientId", clientId);
return this;
}
this.validateToken = function (audiencesFromToken, scopesFromToken, cid) {
foreignMode = false;
var allowedAudiences = extractAudiencesFromToken(audiencesFromToken, scopesFromToken || [], cid);
if (validateSameClientId(cid) === true || validateAudienceOfXsuaaBrokerClone(allowedAudiences) === true || validateDefault(allowedAudiences) === true) {
return ValidationResults.createValid();
}
return ValidationResults.createInvalid("Jwt token with audience: " + util.inspect(allowedAudiences) + " is not issued for these clientIds: " + util.inspect(clientIds) + ".");
}
this.isForeignMode = function () {
return foreignMode;
}
function validateSameClientId(cidFromToken) {
if (!cidFromToken || !clientId) {
return false;
}
return cidFromToken.trim() === clientId.trim();
}
//iterate over all configured clientIds and return true of the cb returns true
function forEachClientId(cb) {
for (var i = 0; i < clientIds.length; ++i) {
if (cb(clientIds[i]) === true) {
return true;
}
}
return null;
}
function validateDefault(allowedAudiences) {
return forEachClientId(function (configuredClientId) {
if (allowedAudiences.includes(configuredClientId)) {
return true;
}
});
}
function validateAudienceOfXsuaaBrokerClone(allowedAudiences) {
var ret = forEachClientId(function (configuredClientId) {
if (configuredClientId.includes("!b")) { //isBrokerClientId
for (var i = 0; i < allowedAudiences.length; ++i) {
var audience = allowedAudiences[i];
if (audience.endsWith("|" + configuredClientId)) {
return true;
}
}
}
});
if (ret === null) {
foreignMode = true;
}
return ret;
}
this.getListOfAudiencesFromToken = function (aud, scopes, cid) {
return extractAudiencesFromToken(aud || [], scopes || [], cid);
}
function extractAudiencesFromToken(aud, scopes, cid) {
var audiences = [];
var tokenAudiences = aud || [];
for (var i = 0; i < tokenAudiences.length; ++i) {
var audience = tokenAudiences[i];
if (audience.indexOf(DOT) > -1) {
// CF UAA derives the audiences from the scopes.
// In case the scopes contains namespaces, these needs to be removed.
var aud = audience.substring(0, audience.indexOf(DOT)).trim();
if (aud && !audiences.includes(aud)) {
audiences.push(aud);
}
} else {
audiences.push(audience);
}
}
if (audiences.length == 0) {
for (var i = 0; i < scopes.length; ++i) {
var scope = scopes[i];
if (scope.indexOf(DOT) > -1) {
var aud = scope.substring(0, scope.indexOf(DOT)).trim();
if (aud && !audiences.includes(aud)) {
audiences.push(aud);
}
}
}
}
if (cid && audiences.indexOf(cid) === -1) {
audiences.push(cid);
}
return audiences;
}
//allow an empty constructor
if (clientId) {
this.configureTrustedClientId(clientId);
}
}
function buildJwksManager(config) {
if(!config) {
return new JwksManager();
}
let jwksExpirationTime = config.expirationTime || JwksReplica.DEFAULT_EXPIRATION_TIME;
let jwksRefreshPeriod = config.refreshPeriod || JwksReplica.DEFAULT_REFRESH_PERIOD;
return new JwksManager().withExpirationTime(jwksExpirationTime).withRefreshPeriod(jwksRefreshPeriod);
}
function JwtTokenValidatorIAS(configArray, serviceCredentials, attributes) {
if(!jwksManager) {
jwksManager = buildJwksManager(serviceCredentials.jwksCache);
}
this.isForeignMode = function () {
return false;
}
this.validateToken = function (accessToken, cb) {
function returnError(code, errorString) {
debugError('\n' + errorString);
var error = new Error(errorString);
error.statuscode = code;
return cb(error);
}
//make sure we have at least an array of 1 domain
const trustedDomains = Array.isArray(serviceCredentials.domains) ? serviceCredentials.domains : [serviceCredentials.domain];
const token = new TokenInfo(accessToken);
//Issuer validation
const issuer = token.getIssuer();
let issuerDomain;
try {
issuerDomain = this.getIssuerDomain(issuer, trustedDomains);
} catch (e) {
return returnError(401, `Issuer validation failed for issuer ${issuer}, message=${e}`);
}
// (optional) x5t validation
if (attributes["x5tValidation"]) {
if (!attributes.x509Certificate) {
return returnError(401, "x5t validation failed (no client certificate found in request header).");
} else {
let x5tValidationResult = X5tValidator.validateToken(token, attributes.x509Certificate);
if (!x5tValidationResult.isValid()) {
return returnError(401, x5tValidationResult.getErrorDescription());
}
}
}
const verificationKeySupplier = async (header, callback) => {
let err, jwk;
try {
const jwks = await jwksManager.getIdentityJwks(issuerDomain, serviceCredentials, token, attributes);
jwk = await jwks.get(header.kid);
} catch(e) {
err = e;
}
callback(err, jwk ? jwk.value : null);
}
return token.verify(verificationKeySupplier, function(err, token) {
if (err) {
debugError('\n' + err.message);
err.statuscode = 401;
return cb(err);
}
//Audience validation
let audienceValidator = new JwtAudienceValidator(serviceCredentials.clientid);
for (var i = 1; i < configArray.length; ++i) {
if (configArray[i] && configArray[i].clientid) {
audienceValidator.configureTrustedClientId(configArray[i].clientid);
}
}
let valid_result = audienceValidator.validateToken(token.getAudiencesArray());
if (!valid_result.isValid()) {
return returnError(401, valid_result.getErrorDescription());
}
cb(null, token);
});
}
/**
* Returns the issuer domain based on the issuer of the token but validates it against a list of trusted domains.
* @param {string} issuer issuer from token
* @param {Array<string>} trustedDomains a list of trusted domains
* @returns domain of issuer if is either a trusted domain or a subdomain of a trusted domain
* @throws Error if issuer is empty, not trusted or not a valid URL
*/
this.getIssuerDomain = function (issuer, trustedDomains = []) {
if(!issuer) {
throw new Error("No issuer found.");
}
const httpsScheme = "https://";
const issuerUrl = issuer.startsWith(httpsScheme) ? issuer : `${httpsScheme}${issuer}`;
try {
new URL(issuerUrl);
} catch(e) {
throw new Error("Issuer was not a valid URL suitable for https.", e);
}
const issuerDomain = issuerUrl.substring(httpsScheme.length);
for(let d of trustedDomains) {
const validSubdomainPattern = `^[a-zA-Z0-9-]{1,63}\\.${escapeStringForRegex(d)}$`; // a string that ends with .<trustedDomain> and contains 1-63 letters, digits or '-' before that for the subdomain
if(issuerDomain === d || issuerDomain.match(new RegExp(validSubdomainPattern))) {
return issuerDomain;
}
}
throw new Error("Issuer domain was not a trusted domain or a subdomain of a trusted domain.")
}
/**
* Escapes Regex special characters in the given string, so that the string can be used for a literal match inside a Regex.
* Regex.escape is only a proposal at the time of writing.
* The source of this code is https://github.com/tc39/proposal-regex-escaping/blob/main/polyfill.js
*/
function escapeStringForRegex(s) {
return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
}
}
/* Adds missing line breaks to malformed PEM keys.
* For backward-compatibility, a specific kind of malformed PEM needs to be supported that is lacking line breaks around the header and footer.
* This kind of PEM input can occur, for example, in old service bindings of XSA and is not always fixable by consumers of this library.
*/
function cleanUpPemKey(pem) {
if(!pem) {
return null;
}
const header = "-----BEGIN PUBLIC KEY-----";
if(!pem.includes(`${header}\n`)) {
pem = pem.replace(header, `${header}\n`);
}
const footer = "-----END PUBLIC KEY-----";
if(!pem.includes(`\n${footer}`)) {
pem = pem.replace(footer, `\n${footer}`);
}
return pem;
}
function JwtTokenValidatorXSUAA(configArray, serviceCredentials, attributes) {
if(!jwksManager) {
jwksManager = buildJwksManager(serviceCredentials.jwksCache);
}
var foreignMode = false;
this.isForeignMode = function () {
return foreignMode;
}
//prepare JWT validators
this.validateToken = function (accessToken, cb) {
function returnError(code, errorString) {
debugError('\n' + errorString);
var error = new Error(errorString);
error.statuscode = code;
return cb(error);
}
let tokeninfo = new TokenExchanger(serviceCredentials);
return tokeninfo.prepareToken(accessToken,
function (err, token) {
if (err) {
debugError('\n' + err.message);
err.statuscode = 401;
return cb(err);
}
let kidNotInJwksErr = null;
const verificationKeySupplier = async (header, callback) => {
let keyFromConfig = cleanUpPemKey(serviceCredentials.verificationkey);
if(!header.jku || !header.kid || header.kid == 'legacy-token-key') {
debugTrace("Token header contained no JKU or KID or the KID was 'legacy-token-key'. Using verification key from service configuration.");
return callback(null, keyFromConfig);
}
let jwk;
try {
const jwks = jwksManager.getXsuaaJwks(serviceCredentials.uaadomain || serviceCredentials.url, token.getZoneId(), attributes);
jwk = await jwks.get(header.kid);
} catch(e) {
if(e.kidMissingInJwks) {
// try validation with keyFromConfig but remember KID was not in JWKS for error logging
kidNotInJwksErr = e;
jwk = keyFromConfig;
} else {
return callback(e);
}
}
return callback(null, jwk ? cleanUpPemKey(jwk.value) : keyFromConfig);
}
return token.verify(verificationKeySupplier,
function (err, token) {
if (err) {
if(kidNotInJwksErr) {
// verification with keyFromConfig as fallback failed after key with KID was not in JWKS
debugError(kidNotInJwksErr);
return cb(kidNotInJwksErr, token);
}
debugError(err.statuscode);
debugError(err.message);
debugError(err.stack);
return cb(err, token);
}
var decodedToken = token.getPayload();
if (!token.getClientId()) {
return returnError(400, 'Client Id not contained in access token. Giving up!');
}
if (!decodedToken.zid) {
return returnError(400, 'Identity Zone not contained in access token. Giving up!');
}
var audienceValidator = new JwtAudienceValidator(configArray[0].clientid);
if (configArray[0].xsappname) {
audienceValidator.configureTrustedClientId(configArray[0].xsappname);
}
for (var i = 1; i < configArray.length; ++i) {
if (configArray[i]) {
if (configArray[i].clientid) {
audienceValidator.configureTrustedClientId(configArray[i].clientid);
}
if (configArray[i].xsappname) {
audienceValidator.configureTrustedClientId(configArray[i].xsappname);
}
}
}
var valid_result = audienceValidator.validateToken(token.getAudiencesArray(), decodedToken.scope, decodedToken.cid);
if (!valid_result.isValid()) {
return returnError(401, valid_result.getErrorDescription());
}
if (configArray[0].clientid !== decodedToken.cid) {
foreignMode = audienceValidator.isForeignMode();
}
cb(null, token);
}
);
})
};
}
function JwtTokenValidatorUAA(configArray, serviceCredentials, attributes) {
if(!jwksManager) {
jwksManager = buildJwksManager(serviceCredentials.jwksCache);
}
var foreignMode = false;
this.isForeignMode = function () {
return foreignMode;
}
//prepare JWT validators
this.validateToken = function (accessToken, cb) {
function returnError(code, errorString) {
debugError('\n' + errorString);
var error = new Error(errorString);
error.statuscode = code;
return cb(error);
}
let tokeninfo = new TokenExchanger(serviceCredentials, true);
return tokeninfo.prepareToken(accessToken,
function (err, token) {
if (err) {
debugError('\n' + err.message);
err.statuscode = 401;
return cb(err);
}
let kidNotInJwksErr = null;
const verificationKeySupplier = async (header, callback) => {
let keyFromConfig = cleanUpPemKey(serviceCredentials.verificationkey);
if(!header.jku || !header.kid || header.kid == 'legacy-token-key') {
debugTrace("Token header contained no JKU or KID or the KID was 'legacy-token-key'. Using verification key from service configuration.");
return callback(null, keyFromConfig);
}
let jwk;
try {
const jwks = jwksManager.getXsuaaJwks(serviceCredentials.uaadomain || serviceCredentials.url, token.getZoneId(), attributes);
jwk = await jwks.get(header.kid);
} catch(e) {
if(e.kidMissingInJwks) {
// try validation with keyFromConfig but remember KID was not in JWKS for error logging
kidNotInJwksErr = e;
jwk = keyFromConfig;
} else {
return callback(e);
}
}
return callback(null, jwk ? cleanUpPemKey(jwk.value) : keyFromConfig);
}
return token.verify(verificationKeySupplier,
function (err, token) {
if (err) {
if(kidNotInJwksErr) {
// verification with keyFromConfig as fallback failed after key with KID was not in JWKS
debugError(kidNotInJwksErr);
return cb(kidNotInJwksErr, token);
}
debugError(err.statuscode);
debugError(err.message);
debugError(err.stack);
return cb(err, token);
}
var decodedToken = token.getPayload();
if (!token.getClientId()) {
return returnError(400, 'Client Id not contained in access token. Giving up!');
}
if (!decodedToken.zid) {
return returnError(400, 'Identity Zone not contained in access token. Giving up!');
}
var audienceValidator = new JwtAudienceValidator(configArray[0].clientid);
if (configArray[0].xsappname) {
audienceValidator.configureTrustedClientId(configArray[0].xsappname);
}
for (var i = 1; i < configArray.length; ++i) {
if (configArray[i]) {
if (configArray[i].clientid) {
audienceValidator.configureTrustedClientId(configArray[i].clientid);
}
if (configArray[i].xsappname) {
audienceValidator.configureTrustedClientId(configArray[i].xsappname);
}
}
}
var valid_result = audienceValidator.validateToken(token.getAudiencesArray(), decodedToken.scope, decodedToken.cid);
if (!valid_result.isValid()) {
return returnError(401, valid_result.getErrorDescription());
}
if (configArray[0].clientid !== decodedToken.cid) {
foreignMode = audienceValidator.isForeignMode();
}
cb(null, token);
}
);
})
};
}
module.exports = {
JwtTokenValidatorIAS: JwtTokenValidatorIAS,
JwtTokenValidatorUAA: JwtTokenValidatorUAA,
JwtTokenValidatorXSUAA: JwtTokenValidatorXSUAA,
JwtAudienceValidator: JwtAudienceValidator
};