@sap-cloud-sdk/core
Version:
SAP Cloud SDK for JavaScript core
397 lines • 17.8 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isUserToken = exports.checkMandatoryValue = exports.readPropertyWithWarn = exports.wrapJwtInHeader = exports.audiences = exports.issuerUrl = exports.verifyJwtWithKey = exports.verificationKeyCache = exports.verifyJwt = exports.retrieveJwt = exports.decodeJwtComplete = exports.decodeJwt = void 0;
var url = __importStar(require("url"));
var util_1 = require("@sap-cloud-sdk/util");
var jsonwebtoken_1 = require("jsonwebtoken");
var environment_accessor_1 = require("./environment-accessor");
var cache_1 = require("./cache");
var verification_keys_1 = require("./verification-keys");
var logger = (0, util_1.createLogger)({
package: 'core',
messageContext: 'jwt'
});
/**
* Decode JWT.
* @param token - JWT to be decoded
* @returns Decoded payload.
*/
function decodeJwt(token) {
return decodeJwtComplete(token).payload;
}
exports.decodeJwt = decodeJwt;
/**
* Decode JWT and return the complete decoded token.
* @param token - JWT to be decoded.
* @returns Decoded token containing payload, header and signature.
* @internal
*/
function decodeJwtComplete(token) {
var decodedToken = (0, jsonwebtoken_1.decode)(token, { complete: true, json: true });
if (decodedToken !== null && isJwtWithPayloadObject(decodedToken)) {
return decodedToken;
}
throw new Error('JwtError: The given jwt payload does not encode valid JSON.');
}
exports.decodeJwtComplete = decodeJwtComplete;
/**
* Retrieve JWT from a request that is based on the node `IncomingMessage`. Fails if no authorization header is given or has the wrong format. Expected format is 'Bearer <TOKEN>'.
* @param req - Request to retrieve the JWT from
* @returns JWT found in header
*/
function retrieveJwt(req) {
var header = authHeader(req);
if (validateAuthHeader(header)) {
return header.split(' ')[1];
}
}
exports.retrieveJwt = retrieveJwt;
function authHeader(req) {
var entries = Object.entries(req.headers).find(function (_a) {
var key = _a[0];
return key.toLowerCase() === 'authorization';
});
if (entries) {
var header = entries[1];
// Header could be a list of headers
return Array.isArray(header) ? header[0] : header;
}
return undefined;
}
function validateAuthHeader(header) {
if (typeof header === 'undefined') {
logger.warn('Authorization header not set.');
return false;
}
var _a = header.split(' '), authType = _a[0], token = _a[1];
if (typeof token === 'undefined') {
logger.warn('Token in auth header missing.');
return false;
}
if (authType.toLowerCase() !== 'bearer') {
logger.warn('Authorization type is not Bearer.');
return false;
}
return true;
}
/**
* Validate the header in the JWT.
* The header should contain a `jku` and `kid` property.
* The URL for fetching the verification key (`jku`) should have the same domain as the XSUAA. So if the UUA domain is "authentication.sap.hana.ondemand.com" the URL should be like
* "http://something.authentication.sap.hana.ondemand.com/somePath" so the host should end with the domain.
* @param header - JWT header.
* @param uaaDomain - Domain given in the XSUAA credentials.
*/
function validateJwtHeaderForVerification(header, uaaDomain) {
if (!header.jku || !header.kid) {
throw new Error('JWT does not contain verification key URL (`jku`) and/or key ID (`kid`).');
}
var jkuDomain = url.parse(header.jku).hostname;
if (!uaaDomain || !jkuDomain || !jkuDomain.endsWith(uaaDomain)) {
throw new Error("The domains of the XSUAA and verification URL do not match. The XSUAA domain is '".concat(uaaDomain, "' and the jku field provided in the JWT is '").concat(jkuDomain, "'."));
}
}
/*
Currently we cannot use the xssec JWT verification, because it does not work with our caching.
Users would not be able to disable the cache for single requests and they could not clear the cache anymore.
Once we use xssec again, some internal behavior will change and it makes sense to document it in the compatibility notes.
Proposal (subject to change):
- [core] Use `@sap/xssec` library for token retrieval and JWT verification which behaves slightly different in some edge cases:
- Fail JWT verification if audiences (`aud`) and/or zone id (`zid`) are missing on the JWT.
- Attempt verification with the verification key in the xsuaa binding, if the xsuaa url and the jku in the JWT don't match, instead of throwing an error directly.
- Attempt verification with the verification key in the xsuaa binding, if `jku` or `kid` are not given in the JWT.
*/
// async function verifyJwtXssec(
// token: string,
// options: VerifyJwtOptions
// ): Promise<any> {
// const xsuaaService = resolveService('xsuaa').credentials;
// if (!options.cacheVerificationKeys) {
// // disable cache
// xsuaaService.keyCache = {
// cacheSize: 0
// };
// }
// return new Promise((resolve, reject) => {
// xssec.createSecurityContext(token, xsuaaService, (error, securityContext) =>
// error ? reject(error) : resolve(securityContext)
// );
// });
// }
/**
* Verifies the given JWT and returns the decoded payload.
* @param token - JWT to be verified
* @param options - Options to control certain aspects of JWT verification behavior.
* @returns A Promise to the decoded and verified JWT.
*/
function verifyJwt(token, options) {
return __awaiter(this, void 0, void 0, function () {
var creds, header, cacheKey, key;
return __generator(this, function (_a) {
options = __assign(__assign({}, defaultVerifyJwtOptions), options);
creds = (0, environment_accessor_1.getXsuaaServiceCredentials)(token);
header = decodeJwtComplete(token).header;
validateJwtHeaderForVerification(header, creds.uaadomain);
cacheKey = buildCacheKey(header.jku, header.kid);
if (options.cacheVerificationKeys) {
key = exports.verificationKeyCache.get(cacheKey);
if (key) {
return [2 /*return*/, verifyJwtWithKey(token, key.value).catch(function (error) {
logger.warn('Unable to verify JWT with cached key, fetching new verification key.');
logger.warn("Original error: ".concat(error.message));
return fetchAndCacheKeyAndVerify(creds, header, token, options);
})];
}
}
return [2 /*return*/, fetchAndCacheKeyAndVerify(creds, header, token, options)]; // Verify only here
});
});
}
exports.verifyJwt = verifyJwt;
function fetchAndCacheKeyAndVerify(creds, header, token, options) {
return __awaiter(this, void 0, void 0, function () {
var key;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getVerificationKey(creds, header).catch(function (error) {
throw new util_1.ErrorWithCause('Failed to verify JWT. Could not retrieve verification key.', error);
})];
case 1:
key = _a.sent();
if (options === null || options === void 0 ? void 0 : options.cacheVerificationKeys) {
exports.verificationKeyCache.set(buildCacheKey(header.jku, header.kid), key);
}
return [2 /*return*/, verifyJwtWithKey(token, key.value)];
}
});
});
}
var defaultVerifyJwtOptions = {
cacheVerificationKeys: true
};
function getVerificationKey(xsuaaCredentials, header) {
return (0, verification_keys_1.fetchVerificationKeys)(xsuaaCredentials, header.jku).then(function (verificationKeys) {
if (!verificationKeys.length) {
throw Error('No verification keys have been returned by the XSUAA service.');
}
var verificationKey = verificationKeys.find(function (key) { return key.keyId === header.kid; });
if (!verificationKey) {
throw new Error('Could not find verification key for the given key ID.');
}
return verificationKey;
});
}
// 15 minutes is the default value used by the xssec lib
exports.verificationKeyCache = new cache_1.Cache({ minutes: 15 });
function buildCacheKey(jku, kid) {
if (!jku || !kid) {
throw new Error('Could not build cache key. `jku` and/or `kid` is not defined.');
}
return jku + kid;
}
/**
* Verifies the given JWT with the given key and returns the decoded payload.
* @param token - JWT to be verified.
* @param key - Key to use for verification.
* @returns A Promise to the decoded and verified JWT.
*/
function verifyJwtWithKey(token, key) {
return new Promise(function (resolve, reject) {
(0, jsonwebtoken_1.verify)(token, sanitizeVerificationKey(key), function (err, decodedToken) {
if (err) {
return reject(new util_1.ErrorWithCause('Invalid JWT.', err));
}
if (!decodedToken) {
return reject('Invalid JWT. Token verification yielded `undefined`.');
}
if (typeof decodedToken === 'string') {
return resolve(JSON.parse(decodedToken));
}
return resolve(decodedToken);
});
});
}
exports.verifyJwtWithKey = verifyJwtWithKey;
function sanitizeVerificationKey(key) {
// Add new line after -----BEGIN PUBLIC KEY----- and before -----END PUBLIC KEY----- because the lib won't work otherwise
return key
.replace(/\n/g, '')
.replace(/(KEY\s*-+)([^\n-])/, '$1\n$2')
.replace(/([^\n-])(-+\s*END)/, '$1\n$2');
}
/**
* Get the issuer URL of a decoded JWT.
* @param decodedToken - Token to read the issuer URL from.
* @returns The issuer URL if available.
*/
function issuerUrl(decodedToken) {
return readPropertyWithWarn(decodedToken, 'iss');
}
exports.issuerUrl = issuerUrl;
/**
* Retrieve the audiences of a decoded JWT based on the audiences and scopes in the token.
* @param decodedToken - Token to retrieve the audiences from.
* @returns A set of audiences.
*/
// Comments taken from the Java SDK implementation
// Currently, scopes containing dots are allowed.
// Since the UAA builds audiences by taking the substring of scopes up to the last dot,
// Scopes with dots will lead to an incorrect audience which is worked around here.
// If a JWT contains no audience, infer audiences based on the scope names in the JWT.
// This is currently necessary as the UAA does not correctly fill the audience in the user token flow.
function audiences(decodedToken) {
if (audiencesFromAud(decodedToken).length) {
return new Set(audiencesFromAud(decodedToken));
}
return new Set(audiencesFromScope(decodedToken));
}
exports.audiences = audiences;
function audiencesFromAud(decodedToken) {
if (!(decodedToken.aud instanceof Array && decodedToken.aud.length)) {
return [];
}
return decodedToken.aud.map(function (aud) {
return aud.includes('.') ? aud.substr(0, aud.indexOf('.')) : aud;
});
}
function audiencesFromScope(decodedToken) {
if (!decodedToken.scope) {
return [];
}
var scopes = decodedToken.scope instanceof Array
? decodedToken.scope
: [decodedToken.scope];
return scopes.reduce(function (aud, scope) {
if (scope.includes('.')) {
return __spreadArray(__spreadArray([], aud, true), [scope.substr(0, scope.indexOf('.'))], false);
}
return aud;
}, []);
}
/**
* Wraps the access token in header's authorization.
* @param token - Token to attach in request header
* @returns The request header that holds the access token
*/
function wrapJwtInHeader(token) {
return { headers: { Authorization: 'Bearer ' + token } };
}
exports.wrapJwtInHeader = wrapJwtInHeader;
function readPropertyWithWarn(jwtPayload, property) {
if (!jwtPayload[property]) {
logger.warn("WarningJWT: The provided JWT payload does not include a '".concat(property, "' property."));
}
return jwtPayload[property];
}
exports.readPropertyWithWarn = readPropertyWithWarn;
/**
* Checks if a given key is present in the decoded JWT. If not, an error is thrown.
* @param key - The key of the representation in typescript
* @param mapping - The mapping between the typescript keys and the JWT key
* @param jwtPayload - JWT payload to check fo the given key.
*/
function checkMandatoryValue(key, mapping, jwtPayload) {
var value = mapping[key].extractorFunction(jwtPayload);
if (!value) {
throw new Error("Property '".concat(mapping[key].keyInJwt, "' is missing in JWT payload."));
}
}
exports.checkMandatoryValue = checkMandatoryValue;
/**
* The user JWT can be a full JWT containing user information but also a reduced one setting only the iss value
* This method divides the two cases.
* @param token - Token to be investigated
* @returns Boolean value with true if the input is a UserJwtPair
*/
function isUserToken(token) {
if (!token) {
return false;
}
// Check if it is an Issuer Payload
var keys = Object.keys(token.decoded);
return !(keys.length === 1 && keys[0] === 'iss');
}
exports.isUserToken = isUserToken;
function isJwtWithPayloadObject(decoded) {
return typeof decoded.payload !== 'string';
}
//# sourceMappingURL=jwt.js.map
;