diff --git a/libraries/botframework-connector/src/auth/jwtTokenExtractor.ts b/libraries/botframework-connector/src/auth/jwtTokenExtractor.ts index e09ff22d1b..31e031839c 100644 --- a/libraries/botframework-connector/src/auth/jwtTokenExtractor.ts +++ b/libraries/botframework-connector/src/auth/jwtTokenExtractor.ts @@ -34,22 +34,32 @@ export class JwtTokenExtractor { * @param metadataUrl Metadata Url. * @param allowedSigningAlgorithms Allowed signing algorithms. * @param proxySettings The proxy settings for the request. + * @param tokenRefreshInterval The token refresh interval in hours. The default value is 24 hours. */ constructor( tokenValidationParameters: VerifyOptions, metadataUrl: string, allowedSigningAlgorithms: string[] | Algorithm[], proxySettings?: ProxySettings, + tokenRefreshInterval?: number, ) { this.tokenValidationParameters = { ...tokenValidationParameters }; this.tokenValidationParameters.algorithms = allowedSigningAlgorithms as Algorithm[]; - this.openIdMetadata = JwtTokenExtractor.getOrAddOpenIdMetadata(metadataUrl, proxySettings); + this.openIdMetadata = JwtTokenExtractor.getOrAddOpenIdMetadata( + metadataUrl, + proxySettings, + tokenRefreshInterval, + ); } - private static getOrAddOpenIdMetadata(metadataUrl: string, proxySettings?: ProxySettings): OpenIdMetadata { + private static getOrAddOpenIdMetadata( + metadataUrl: string, + proxySettings?: ProxySettings, + tokenRefreshInterval?: number, + ): OpenIdMetadata { let metadata = this.openIdMetadataCache.get(metadataUrl); if (!metadata) { - metadata = new OpenIdMetadata(metadataUrl, proxySettings); + metadata = new OpenIdMetadata(metadataUrl, proxySettings, tokenRefreshInterval); this.openIdMetadataCache.set(metadataUrl, metadata); } diff --git a/libraries/botframework-connector/src/auth/openIdMetadata.ts b/libraries/botframework-connector/src/auth/openIdMetadata.ts index b8edd49a1e..7357979d8b 100644 --- a/libraries/botframework-connector/src/auth/openIdMetadata.ts +++ b/libraries/botframework-connector/src/auth/openIdMetadata.ts @@ -26,10 +26,12 @@ export class OpenIdMetadata { * * @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, ) {} /** @@ -39,8 +41,8 @@ export class OpenIdMetadata { * @returns A `Promise` representation for either a [IOpenIdMetadataKey](botframework-connector:module.IOpenIdMetadataKey) or `null`. */ async getKey(keyId: string): Promise { - // If keys are more than 24 hours old, refresh them - if (this.lastUpdated < Date.now() - 1000 * 60 * 60 * 24) { + // 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 diff --git a/libraries/botframework-connector/src/auth/parameterizedBotFrameworkAuthentication.ts b/libraries/botframework-connector/src/auth/parameterizedBotFrameworkAuthentication.ts index 4b5ddb5288..88efc546f7 100644 --- a/libraries/botframework-connector/src/auth/parameterizedBotFrameworkAuthentication.ts +++ b/libraries/botframework-connector/src/auth/parameterizedBotFrameworkAuthentication.ts @@ -306,6 +306,7 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent this.toBotFromEmulatorOpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms, this.connectorClientOptions?.proxySettings, + this.connectorClientOptions?.tokenRefreshInterval, ); const parts: string[] = authHeader.split(' '); @@ -393,6 +394,7 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent this.toBotFromEmulatorOpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms, this.connectorClientOptions?.proxySettings, + this.connectorClientOptions?.tokenRefreshInterval, ); const identity: ClaimsIdentity = await tokenExtractor.getIdentityFromAuthHeader( @@ -480,6 +482,7 @@ export class ParameterizedBotFrameworkAuthentication extends BotFrameworkAuthent this.toBotFromChannelOpenIdMetadataUrl, AuthenticationConstants.AllowedSigningAlgorithms, this.connectorClientOptions?.proxySettings, + this.connectorClientOptions?.tokenRefreshInterval, ); const identity: ClaimsIdentity = await tokenExtractor.getIdentityFromAuthHeader( diff --git a/libraries/botframework-connector/src/connectorApi/models/index.ts b/libraries/botframework-connector/src/connectorApi/models/index.ts index 649e6ab3fe..bf9fb6dd45 100644 --- a/libraries/botframework-connector/src/connectorApi/models/index.ts +++ b/libraries/botframework-connector/src/connectorApi/models/index.ts @@ -17,6 +17,11 @@ export interface ConnectorClientOptions extends ServiceClientOptions { * HTTP and HTTPS agents which will be used for every HTTP request (Node.js only). */ agentSettings?: AgentSettings; + + /** + * Token refresh interval in hours used to determine when to refresh the token cache. The default value is 24 hours. + */ + tokenRefreshInterval?: number; } /** diff --git a/libraries/botframework-connector/tests/openIdMetadata.test.js b/libraries/botframework-connector/tests/openIdMetadata.test.js index de5e4b7b52..b87a283054 100644 --- a/libraries/botframework-connector/tests/openIdMetadata.test.js +++ b/libraries/botframework-connector/tests/openIdMetadata.test.js @@ -78,7 +78,7 @@ GOG4x32vEzakArLPxAKwGvkvu0jToAyvSQIDAQAB assert.equal(foundKey.key, publicKey); }); - it('calls refreshCache if lastUpdated < (1000 * 60 * 60 * 24)', async function () { + it('calls refreshCache if lastUpdated < refreshInterval (default 24)', async function () { let nockScope = setupNockCalls(); const openIdMetadata = new OpenIdMetadata(mockUrl + mockMetadataUrl); @@ -93,6 +93,41 @@ GOG4x32vEzakArLPxAKwGvkvu0jToAyvSQIDAQAB assert(nockScope.isDone(), 'nock calls not completed'); assert.equal(foundKey.key, publicKey); }); + + it('calls refreshCache if lastUpdated > refreshInterval', async function () { + let nockScope = setupNockCalls(); + + const tokenRefreshInterval = 3; // 3 hours + + const openIdMetadata = new OpenIdMetadata(mockUrl + mockMetadataUrl, null, tokenRefreshInterval); + let foundKey = await openIdMetadata.getKey(kid); + assert(nockScope.isDone(), 'nock calls not completed'); + + assert.equal(foundKey.key, publicKey); + openIdMetadata.lastUpdated = Date.now() - 1000 * 60 * 60 * 4; + + nockScope = setupNockCalls(); + foundKey = await openIdMetadata.getKey(kid); + assert(nockScope.isDone(), 'nock calls not completed'); + assert.equal(foundKey.key, publicKey); + }); + + it('retrieves cached key if lastUpdated < refreshInterval', async function () { + let nockScope = setupNockCalls(); + + const tokenRefreshInterval = 3; // 3 hours + + const openIdMetadata = new OpenIdMetadata(mockUrl + mockMetadataUrl, null, tokenRefreshInterval); + let foundKey = await openIdMetadata.getKey(kid); + assert(nockScope.isDone(), 'nock calls not completed'); + + assert.equal(foundKey.key, publicKey); + openIdMetadata.lastUpdated = Date.now() - 1000 * 60 * 60 * 2; + + nockScope = setupNockCalls(); + foundKey = await openIdMetadata.getKey(kid); + assert.equal(foundKey.key, publicKey); + }); }); function setupNockCalls() {