UNPKG

17.1 kBJavaScriptView Raw
1"use strict";
2var __assign = (this && this.__assign) || function () {
3 __assign = Object.assign || function(t) {
4 for (var s, i = 1, n = arguments.length; i < n; i++) {
5 s = arguments[i];
6 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7 t[p] = s[p];
8 }
9 return t;
10 };
11 return __assign.apply(this, arguments);
12};
13var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14 if (k2 === undefined) k2 = k;
15 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
16}) : (function(o, m, k, k2) {
17 if (k2 === undefined) k2 = k;
18 o[k2] = m[k];
19}));
20var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21 Object.defineProperty(o, "default", { enumerable: true, value: v });
22}) : function(o, v) {
23 o["default"] = v;
24});
25var __importStar = (this && this.__importStar) || function (mod) {
26 if (mod && mod.__esModule) return mod;
27 var result = {};
28 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
29 __setModuleDefault(result, mod);
30 return result;
31};
32var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
33 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
34 return new (P || (P = Promise))(function (resolve, reject) {
35 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
36 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
37 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
38 step((generator = generator.apply(thisArg, _arguments || [])).next());
39 });
40};
41var __generator = (this && this.__generator) || function (thisArg, body) {
42 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
43 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
44 function verb(n) { return function (v) { return step([n, v]); }; }
45 function step(op) {
46 if (f) throw new TypeError("Generator is already executing.");
47 while (_) try {
48 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;
49 if (y = 0, t) op = [op[0] & 2, t.value];
50 switch (op[0]) {
51 case 0: case 1: t = op; break;
52 case 4: _.label++; return { value: op[1], done: false };
53 case 5: _.label++; y = op[1]; op = [0]; continue;
54 case 7: op = _.ops.pop(); _.trys.pop(); continue;
55 default:
56 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
57 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
58 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
59 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
60 if (t[2]) _.ops.pop();
61 _.trys.pop(); continue;
62 }
63 op = body.call(thisArg, _);
64 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
65 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
66 }
67};
68var __spreadArray = (this && this.__spreadArray) || function (to, from) {
69 for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
70 to[j] = from[i];
71 return to;
72};
73Object.defineProperty(exports, "__esModule", { value: true });
74exports.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;
75var url = __importStar(require("url"));
76var util_1 = require("@sap-cloud-sdk/util");
77var jsonwebtoken_1 = require("jsonwebtoken");
78var environment_accessor_1 = require("./environment-accessor");
79var cache_1 = require("./cache");
80var verification_keys_1 = require("./verification-keys");
81var logger = util_1.createLogger({
82 package: 'core',
83 messageContext: 'jwt'
84});
85/**
86 * Decode JWT.
87 * @param token - JWT to be decoded
88 * @returns Decoded payload.
89 */
90function decodeJwt(token) {
91 return decodeJwtComplete(token).payload;
92}
93exports.decodeJwt = decodeJwt;
94/**
95 * Decode JWT and return the complete decoded token.
96 * @param token - JWT to be decoded.
97 * @returns Decoded token containing payload, header and signature.
98 */
99function decodeJwtComplete(token) {
100 var decodedToken = jsonwebtoken_1.decode(token, { complete: true });
101 if (decodedToken === null || typeof decodedToken === 'string') {
102 throw new Error('JwtError: The given jwt payload does not encode valid JSON.');
103 }
104 return decodedToken;
105}
106exports.decodeJwtComplete = decodeJwtComplete;
107/**
108 * 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>'.
109 * @param req - Request to retrieve the JWT from
110 * @returns JWT found in header
111 */
112function retrieveJwt(req) {
113 var header = authHeader(req);
114 if (validateAuthHeader(header)) {
115 return header.split(' ')[1];
116 }
117}
118exports.retrieveJwt = retrieveJwt;
119function authHeader(req) {
120 var entries = Object.entries(req.headers).find(function (_a) {
121 var key = _a[0];
122 return key.toLowerCase() === 'authorization';
123 });
124 if (entries) {
125 var header = entries[1];
126 // Header could be a list of headers
127 return Array.isArray(header) ? header[0] : header;
128 }
129 return undefined;
130}
131function validateAuthHeader(header) {
132 if (typeof header === 'undefined') {
133 logger.warn('Authorization header not set.');
134 return false;
135 }
136 var _a = header.split(' '), authType = _a[0], token = _a[1];
137 if (typeof token === 'undefined') {
138 logger.warn('Token in auth header missing.');
139 return false;
140 }
141 if (authType.toLowerCase() !== 'bearer') {
142 logger.warn('Authorization type is not Bearer.');
143 return false;
144 }
145 return true;
146}
147/**
148 * Validate the header in the JWT.
149 * The header should contain a `jku` and `kid` property.
150 * 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
151 * "http://something.authentication.sap.hana.ondemand.com/somePath" so the host should end with the domain.
152 * @param header - JWT header.
153 * @param uaaDomain - Domain given in the XSUAA credentials.
154 */
155function validateJwtHeaderForVerification(header, uaaDomain) {
156 if (!header.jku || !header.kid) {
157 throw new Error('JWT does not contain verification key URL (`jku`) and/or key ID (`kid`).');
158 }
159 var jkuDomain = url.parse(header.jku).hostname;
160 if (!uaaDomain || !jkuDomain || !jkuDomain.endsWith(uaaDomain)) {
161 throw new Error("The domains of the XSUAA and verification URL do not match. The XSUAA domain is '" + uaaDomain + "' and the jku field provided in the JWT is '" + jkuDomain + "'.");
162 }
163}
164/*
165 Currently we cannot use the xssec JWT verification, because it does not work with our caching.
166 Users would not be able to disable the cache for single requests and they could not clear the cache anymore.
167 Once we use xssec again, some internal behavior will change and it makes sense to document it in the compatibility notes.
168 Proposal (subject to change):
169 - [core] Use `@sap/xssec` library for token retrieval and JWT verification which behaves slightly different in some edge cases:
170 - Fail JWT verification if audiences (`aud`) and/or zone id (`zid`) are missing on the JWT.
171 - 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.
172 - Attempt verification with the verification key in the xsuaa binding, if `jku` or `kid` are not given in the JWT.
173 */
174// async function verifyJwtXssec(
175// token: string,
176// options: VerifyJwtOptions
177// ): Promise<any> {
178// const xsuaaService = resolveService('xsuaa').credentials;
179// if (!options.cacheVerificationKeys) {
180// // disable cache
181// xsuaaService.keyCache = {
182// cacheSize: 0
183// };
184// }
185// return new Promise((resolve, reject) => {
186// xssec.createSecurityContext(token, xsuaaService, (error, securityContext) =>
187// error ? reject(error) : resolve(securityContext)
188// );
189// });
190// }
191/**
192 * Verifies the given JWT and returns the decoded payload.
193 * @param token - JWT to be verified
194 * @param options - Options to control certain aspects of JWT verification behavior.
195 * @returns A Promise to the decoded and verified JWT.
196 */
197function verifyJwt(token, options) {
198 return __awaiter(this, void 0, void 0, function () {
199 var creds, header, cacheKey, key;
200 return __generator(this, function (_a) {
201 options = __assign(__assign({}, defaultVerifyJwtOptions), options);
202 creds = environment_accessor_1.getXsuaaServiceCredentials(token);
203 header = decodeJwtComplete(token).header;
204 validateJwtHeaderForVerification(header, creds.uaadomain);
205 cacheKey = buildCacheKey(header.jku, header.kid);
206 if (options.cacheVerificationKeys) {
207 key = exports.verificationKeyCache.get(cacheKey);
208 if (key) {
209 return [2 /*return*/, verifyJwtWithKey(token, key.value).catch(function (error) {
210 logger.warn('Unable to verify JWT with cached key, fetching new verification key.');
211 logger.warn("Original error: " + error.message);
212 return fetchAndCacheKeyAndVerify(creds, header, token, options);
213 })];
214 }
215 }
216 return [2 /*return*/, fetchAndCacheKeyAndVerify(creds, header, token, options)]; // Verify only here
217 });
218 });
219}
220exports.verifyJwt = verifyJwt;
221function fetchAndCacheKeyAndVerify(creds, header, token, options) {
222 return __awaiter(this, void 0, void 0, function () {
223 var key;
224 return __generator(this, function (_a) {
225 switch (_a.label) {
226 case 0: return [4 /*yield*/, getVerificationKey(creds, header).catch(function (error) {
227 throw new util_1.ErrorWithCause('Failed to verify JWT. Could not retrieve verification key.', error);
228 })];
229 case 1:
230 key = _a.sent();
231 if (options === null || options === void 0 ? void 0 : options.cacheVerificationKeys) {
232 exports.verificationKeyCache.set(buildCacheKey(header.jku, header.kid), key);
233 }
234 return [2 /*return*/, verifyJwtWithKey(token, key.value)];
235 }
236 });
237 });
238}
239var defaultVerifyJwtOptions = {
240 cacheVerificationKeys: true
241};
242function getVerificationKey(xsuaaCredentials, header) {
243 return verification_keys_1.fetchVerificationKeys(xsuaaCredentials, header.jku).then(function (verificationKeys) {
244 if (!verificationKeys.length) {
245 throw Error('No verification keys have been returned by the XSUAA service.');
246 }
247 var verificationKey = verificationKeys.find(function (key) { return key.keyId === header.kid; });
248 if (!verificationKey) {
249 throw new Error('Could not find verification key for the given key ID.');
250 }
251 return verificationKey;
252 });
253}
254// 15 minutes is the default value used by the xssec lib
255exports.verificationKeyCache = new cache_1.Cache({ minutes: 15 });
256function buildCacheKey(jku, kid) {
257 if (!jku || !kid) {
258 throw new Error('Could not build cache key. `jku` and/or `kid` is not defined.');
259 }
260 return jku + kid;
261}
262/**
263 * Verifies the given JWT with the given key and returns the decoded payload.
264 * @param token - JWT to be verified.
265 * @param key - Key to use for verification.
266 * @returns A Promise to the decoded and verified JWT.
267 */
268function verifyJwtWithKey(token, key) {
269 return new Promise(function (resolve, reject) {
270 jsonwebtoken_1.verify(token, sanitizeVerificationKey(key), function (err, decodedToken) {
271 if (err) {
272 return reject(new util_1.ErrorWithCause('Invalid JWT.', err));
273 }
274 if (!decodedToken) {
275 return reject('Invalid JWT. Token verification yielded `undefined`.');
276 }
277 return resolve(decodedToken);
278 });
279 });
280}
281exports.verifyJwtWithKey = verifyJwtWithKey;
282function sanitizeVerificationKey(key) {
283 // Add new line after -----BEGIN PUBLIC KEY----- and before -----END PUBLIC KEY----- because the lib won't work otherwise
284 return key
285 .replace(/\n/g, '')
286 .replace(/(KEY\s*-+)([^\n-])/, '$1\n$2')
287 .replace(/([^\n-])(-+\s*END)/, '$1\n$2');
288}
289/**
290 * Get the issuer URL of a decoded JWT.
291 * @param decodedToken - Token to read the issuer URL from.
292 * @returns The issuer URL if available.
293 */
294function issuerUrl(decodedToken) {
295 return readPropertyWithWarn(decodedToken, 'iss');
296}
297exports.issuerUrl = issuerUrl;
298/**
299 * Retrieve the audiences of a decoded JWT based on the audiences and scopes in the token.
300 * @param decodedToken - Token to retrieve the audiences from.
301 * @returns A set of audiences.
302 */
303// Comments taken from the Java SDK implementation
304// Currently, scopes containing dots are allowed.
305// Since the UAA builds audiences by taking the substring of scopes up to the last dot,
306// Scopes with dots will lead to an incorrect audience which is worked around here.
307// If a JWT contains no audience, infer audiences based on the scope names in the JWT.
308// This is currently necessary as the UAA does not correctly fill the audience in the user token flow.
309function audiences(decodedToken) {
310 if (audiencesFromAud(decodedToken).length) {
311 return new Set(audiencesFromAud(decodedToken));
312 }
313 return new Set(audiencesFromScope(decodedToken));
314}
315exports.audiences = audiences;
316function audiencesFromAud(decodedToken) {
317 if (!(decodedToken.aud instanceof Array && decodedToken.aud.length)) {
318 return [];
319 }
320 return decodedToken.aud.map(function (aud) {
321 return aud.includes('.') ? aud.substr(0, aud.indexOf('.')) : aud;
322 });
323}
324function audiencesFromScope(decodedToken) {
325 if (!decodedToken.scope) {
326 return [];
327 }
328 var scopes = decodedToken.scope instanceof Array
329 ? decodedToken.scope
330 : [decodedToken.scope];
331 return scopes.reduce(function (aud, scope) {
332 if (scope.includes('.')) {
333 return __spreadArray(__spreadArray([], aud), [scope.substr(0, scope.indexOf('.'))]);
334 }
335 return aud;
336 }, []);
337}
338/**
339 * Wraps the access token in header's authorization.
340 * @param token - Token to attach in request header
341 * @returns The request header that holds the access token
342 */
343function wrapJwtInHeader(token) {
344 return { headers: { Authorization: 'Bearer ' + token } };
345}
346exports.wrapJwtInHeader = wrapJwtInHeader;
347function readPropertyWithWarn(jwtPayload, property) {
348 if (!jwtPayload[property]) {
349 logger.warn("WarningJWT: The provided JWT payload does not include a '" + property + "' property.");
350 }
351 return jwtPayload[property];
352}
353exports.readPropertyWithWarn = readPropertyWithWarn;
354/**
355 * Checks if a given key is present in the decoded JWT. If not, an error is thrown.
356 * @param key - The key of the representation in typescript
357 * @param mapping - The mapping between the typescript keys and the JWT key
358 * @param jwtPayload - JWT payload to check fo the given key.
359 */
360function checkMandatoryValue(key, mapping, jwtPayload) {
361 var value = mapping[key].extractorFunction(jwtPayload);
362 if (!value) {
363 throw new Error("Property '" + mapping[key].keyInJwt + "' is missing in JWT payload.");
364 }
365}
366exports.checkMandatoryValue = checkMandatoryValue;
367/**
368 * The user JWT can be a full JWT containing user information but also a reduced one setting only the iss value
369 * This method divides the two cases.
370 * @param token - Token to be investigated
371 * @returns Boolean value with true if the input is a UserJwtPair
372 */
373function isUserToken(token) {
374 if (!token) {
375 return false;
376 }
377 // Check if it is an Issuer Payload
378 var keys = Object.keys(token.decoded);
379 return !(keys.length === 1 && keys[0] === 'iss');
380}
381exports.isUserToken = isUserToken;
382//# sourceMappingURL=jwt.js.map
\No newline at end of file