botframework-connector
Version:
Bot Connector is autorest generated connector client.
151 lines (133 loc) • 4.53 kB
text/typescript
/**
* @module botframework-connector
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import getPem from 'rsa-pem-from-mod-exp';
import base64url from 'base64url';
import fetch from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { AuthenticationError } from './authenticationError';
import { StatusCodes } from 'botframework-schema';
import { ProxySettings } from 'botbuilder-stdlib/lib/azureCoreHttpCompat';
/**
* Class in charge of manage OpenId metadata.
*/
export class OpenIdMetadata {
private lastUpdated = 0;
private keys: IKey[];
/**
* Initializes a new instance of the [OpenIdMetadata](xref:botframework-connector.OpenIdMetadata) class.
*
* @param url Metadata Url.
* @param proxySettings The proxy settings for the request.
* @param tokenRefreshInterval The token refresh interval in hours. The default value is 24 hours.
*/
constructor(
private url: string,
private proxySettings?: ProxySettings,
private tokenRefreshInterval: number = 24,
) {}
/**
* Gets the Signing key.
*
* @param keyId The key ID to search for.
* @returns A `Promise` representation for either a [IOpenIdMetadataKey](botframework-connector:module.IOpenIdMetadataKey) or `null`.
*/
async getKey(keyId: string): Promise<IOpenIdMetadataKey | null> {
// If keys are older than the refresh interval (default 24 hours), refresh them
if (this.lastUpdated < Date.now() - this.tokenRefreshInterval * 1000 * 60 * 60) {
await this.refreshCache();
// Search the cache even if we failed to refresh
const key = this.findKey(keyId);
return key;
} else {
// Otherwise read from cache
const key = this.findKey(keyId);
// Refresh the cache if a key is not found (max once per hour)
if (!key && this.lastUpdated < Date.now() - 1000 * 60 * 60) {
await this.refreshCache();
return this.findKey(keyId);
}
return key;
}
}
/**
* @private
*/
private async refreshCache(): Promise<void> {
let agent = null;
if (this.proxySettings) {
const proxyUrl = `http://${this.proxySettings.host}:${this.proxySettings.port}`;
agent = new HttpsProxyAgent(proxyUrl);
}
const res = await fetch(this.url, { agent: agent });
if (res.ok) {
const openIdConfig = (await res.json()) as IOpenIdConfig;
const getKeyResponse = await fetch(openIdConfig.jwks_uri, { agent: agent });
if (getKeyResponse.ok) {
this.lastUpdated = new Date().getTime();
this.keys = (await (getKeyResponse.json() as Promise<IOpenIdResponse>)).keys;
} else {
throw new AuthenticationError(
`Failed to load Keys: ${getKeyResponse.status}`,
StatusCodes.INTERNAL_SERVER_ERROR,
);
}
} else {
throw new AuthenticationError(
`Failed to load openID config: ${res.status}`,
StatusCodes.INTERNAL_SERVER_ERROR,
);
}
}
/**
* @private
*/
private findKey(keyId: string): IOpenIdMetadataKey | null {
if (!this.keys) {
return null;
}
for (const key of this.keys) {
if (key.kid === keyId) {
if (!key.n || !key.e) {
// Return null for non-RSA keys
return null;
}
const modulus = base64url.toBase64(key.n);
const exponent = key.e;
return {
key: getPem(modulus, exponent),
endorsements: key.endorsements,
};
}
}
return null;
}
}
interface IOpenIdConfig {
issuer: string;
authorization_endpoint: string;
jwks_uri: string;
id_token_signing_alg_values_supported: string[];
token_endpoint_auth_methods_supported: string[];
}
interface IOpenIdResponse {
keys: IKey[];
}
interface IKey {
kty: string;
use: string;
kid: string;
x5t: string;
n: string;
e: string;
x5c: string[];
endorsements?: string[];
}
export interface IOpenIdMetadataKey {
key: string;
endorsements?: string[];
}