173 lines
No EOL
9 KiB
JavaScript
173 lines
No EOL
9 KiB
JavaScript
"use strict";
|
|
/**
|
|
* @module botframework-connector
|
|
*/
|
|
/**
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
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());
|
|
});
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.JwtTokenExtractor = void 0;
|
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
const claimsIdentity_1 = require("./claimsIdentity");
|
|
const endorsementsValidator_1 = require("./endorsementsValidator");
|
|
const openIdMetadata_1 = require("./openIdMetadata");
|
|
const authenticationError_1 = require("./authenticationError");
|
|
const botframework_schema_1 = require("botframework-schema");
|
|
/**
|
|
* A JWT token processing class that gets identity information and performs security token validation.
|
|
*/
|
|
class JwtTokenExtractor {
|
|
/**
|
|
* Initializes a new instance of the [JwtTokenExtractor](xref:botframework-connector.JwtTokenExtractor) class. Extracts relevant data from JWT Tokens.
|
|
*
|
|
* @param tokenValidationParameters Token validation parameters.
|
|
* @param metadataUrl Metadata Url.
|
|
* @param allowedSigningAlgorithms Allowed signing algorithms.
|
|
* @param proxySettings The proxy settings for the request.
|
|
*/
|
|
constructor(tokenValidationParameters, metadataUrl, allowedSigningAlgorithms, proxySettings) {
|
|
this.tokenValidationParameters = Object.assign({}, tokenValidationParameters);
|
|
this.tokenValidationParameters.algorithms = allowedSigningAlgorithms;
|
|
this.openIdMetadata = JwtTokenExtractor.getOrAddOpenIdMetadata(metadataUrl, proxySettings);
|
|
}
|
|
static getOrAddOpenIdMetadata(metadataUrl, proxySettings) {
|
|
let metadata = this.openIdMetadataCache.get(metadataUrl);
|
|
if (!metadata) {
|
|
metadata = new openIdMetadata_1.OpenIdMetadata(metadataUrl, proxySettings);
|
|
this.openIdMetadataCache.set(metadataUrl, metadata);
|
|
}
|
|
return metadata;
|
|
}
|
|
/**
|
|
* Gets the claims identity associated with a request.
|
|
*
|
|
* @param authorizationHeader The raw HTTP header in the format: "Bearer [longString]".
|
|
* @param channelId The Id of the channel being validated in the original request.
|
|
* @param requiredEndorsements The required JWT endorsements.
|
|
* @returns A `Promise` representation for either a [ClaimsIdentity](botframework-connector:module.ClaimsIdentity) or `null`.
|
|
*/
|
|
getIdentityFromAuthHeader(authorizationHeader, channelId, requiredEndorsements) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!authorizationHeader) {
|
|
return null;
|
|
}
|
|
const parts = authorizationHeader.split(' ');
|
|
if (parts.length === 2) {
|
|
return yield this.getIdentity(parts[0], parts[1], channelId, requiredEndorsements || []);
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
/**
|
|
* Gets the claims identity associated with a request.
|
|
*
|
|
* @param scheme The associated scheme.
|
|
* @param parameter The token.
|
|
* @param channelId The Id of the channel being validated in the original request.
|
|
* @param requiredEndorsements The required JWT endorsements.
|
|
* @returns A `Promise` representation for either a [ClaimsIdentity](botframework-connector:module.ClaimsIdentity) or `null`.
|
|
*/
|
|
getIdentity(scheme, parameter, channelId, requiredEndorsements = []) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// No header in correct scheme or no token
|
|
if (scheme !== 'Bearer' || !parameter) {
|
|
return null;
|
|
}
|
|
// Issuer isn't allowed? No need to check signature
|
|
if (!this.hasAllowedIssuer(parameter)) {
|
|
return null;
|
|
}
|
|
return yield this.validateToken(parameter, channelId, requiredEndorsements);
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
hasAllowedIssuer(jwtToken) {
|
|
const payload = (0, jsonwebtoken_1.decode)(jwtToken);
|
|
let issuer;
|
|
if (payload && typeof payload === 'object') {
|
|
issuer = payload.iss;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
if (Array.isArray(this.tokenValidationParameters.issuer)) {
|
|
return this.tokenValidationParameters.issuer.indexOf(issuer) !== -1;
|
|
}
|
|
if (typeof this.tokenValidationParameters.issuer === 'string') {
|
|
return this.tokenValidationParameters.issuer === issuer;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
validateToken(jwtToken, channelId, requiredEndorsements) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let header = {};
|
|
const decodedToken = (0, jsonwebtoken_1.decode)(jwtToken, { complete: true });
|
|
if (decodedToken && typeof decodedToken === 'object') {
|
|
header = decodedToken.header;
|
|
}
|
|
// Update the signing tokens from the last refresh
|
|
const keyId = header.kid;
|
|
const metadata = yield this.openIdMetadata.getKey(keyId);
|
|
if (!metadata) {
|
|
throw new authenticationError_1.AuthenticationError('Signing Key could not be retrieved.', botframework_schema_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
try {
|
|
let decodedPayload = {};
|
|
const verifyResults = (0, jsonwebtoken_1.verify)(jwtToken, metadata.key, this.tokenValidationParameters);
|
|
if (verifyResults && typeof verifyResults === 'object') {
|
|
// Note: casting is necessary here, but we know `object` is loosely equivalent to a Record
|
|
decodedPayload = verifyResults;
|
|
}
|
|
// enforce endorsements in openIdMetadadata if there is any endorsements associated with the key
|
|
const endorsements = metadata.endorsements;
|
|
if (Array.isArray(endorsements) && endorsements.length !== 0) {
|
|
const isEndorsed = endorsementsValidator_1.EndorsementsValidator.validate(channelId, endorsements);
|
|
if (!isEndorsed) {
|
|
throw new authenticationError_1.AuthenticationError(`Could not validate endorsement for key: ${keyId} with endorsements: ${endorsements.join(',')}`, botframework_schema_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
// Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well
|
|
const additionalEndorsementsSatisfied = requiredEndorsements.every((endorsement) => endorsementsValidator_1.EndorsementsValidator.validate(endorsement, endorsements));
|
|
if (!additionalEndorsementsSatisfied) {
|
|
throw new authenticationError_1.AuthenticationError(`Could not validate additional endorsement for key: ${keyId} with endorsements: ${requiredEndorsements.join(',')}. Expected endorsements: ${requiredEndorsements.join(',')}`, botframework_schema_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
}
|
|
if (this.tokenValidationParameters.algorithms) {
|
|
if (this.tokenValidationParameters.algorithms.indexOf(header.alg) === -1) {
|
|
throw new authenticationError_1.AuthenticationError(`"Token signing algorithm '${header.alg}' not in allowed list`, botframework_schema_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
}
|
|
const claims = Object.entries(decodedPayload).map(([type, value]) => ({ type, value }));
|
|
// Note: true is used here to indicate that these claims are to be considered authenticated. They are sourced
|
|
// from a validated JWT (see `verify` above), so no harm in doing so.
|
|
return new claimsIdentity_1.ClaimsIdentity(claims, true);
|
|
}
|
|
catch (err) {
|
|
if (err.name === 'TokenExpiredError') {
|
|
console.error(err);
|
|
throw new authenticationError_1.AuthenticationError('The token has expired', botframework_schema_1.StatusCodes.UNAUTHORIZED);
|
|
}
|
|
console.error(`Error finding key for token. Available keys: ${metadata.key}`);
|
|
throw err;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
exports.JwtTokenExtractor = JwtTokenExtractor;
|
|
// Cache for OpenIdConnect configuration managers (one per metadata URL)
|
|
JwtTokenExtractor.openIdMetadataCache = new Map();
|
|
//# sourceMappingURL=jwtTokenExtractor.js.map
|