botframework-connector
Version:
Bot Connector is autorest generated connector client.
173 lines • 9.21 kB
JavaScript
;
/**
* @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