diff --git a/sdk/communication/communication-common/README.md b/sdk/communication/communication-common/README.md index f70f5aa82360..80c2c487fd2e 100644 --- a/sdk/communication/communication-common/README.md +++ b/sdk/communication/communication-common/README.md @@ -62,6 +62,19 @@ const tokenCredential = new AzureCommunicationTokenCredential({ }); ``` +### Create a credential with proactive refreshing and custom refresh time + +Optionally, you can adjust the time span before token expiry that will trigger the proactive refreshing of the token. For example, if the proactive refreshing is enabled (`refreshProactively` is true), setting the time span (`refreshIntervalBeforeTokenExpiryInSeconds`) to 5 minutes means that 5 minutes before the cached token expires, the proactive refresh will request a new token by calling the `tokenRefresher` callback. +The default value is 4.5 minutes. This value ensures that when the 'tokenRefresher' callback is internally using MSAL, MSAL avoids the cache and gets a fresh token. + +```typescript +const tokenCredential = new AzureCommunicationTokenCredential({ + tokenRefresher: async () => fetchTokenFromMyServerForUser("bob@contoso.com"), + refreshProactively: true, + refreshIntervalBeforeTokenExpiryInSeconds: 5 * 60 // 5 minutes +}); +``` + ### Create a credential with proactive refreshing and an initial token Passing `initialToken` is an optional optimization to skip the first call to `tokenRefresher`. You can use this to separate the boot from your application from subsequent token refresh cycles. diff --git a/sdk/communication/communication-common/review/communication-common.api.md b/sdk/communication/communication-common/review/communication-common.api.md index ae3575ec1935..9b4f8e924e34 100644 --- a/sdk/communication/communication-common/review/communication-common.api.md +++ b/sdk/communication/communication-common/review/communication-common.api.md @@ -16,7 +16,7 @@ export class AzureCommunicationTokenCredential implements CommunicationTokenCred constructor(refreshOptions: CommunicationTokenRefreshOptions); dispose(): void; getToken(options?: CommunicationGetTokenOptions): Promise; - } +} // @public export interface CommunicationGetTokenOptions { @@ -37,6 +37,7 @@ export interface CommunicationTokenCredential { // @public export interface CommunicationTokenRefreshOptions { + refreshIntervalBeforeTokenExpiryInSeconds?: number; refreshProactively?: boolean; token?: string; tokenRefresher: (abortSignal?: AbortSignalLike) => Promise; @@ -162,7 +163,6 @@ export type UrlWithCredential = { credential: TokenCredential | KeyCredential; }; - // (No @packageDocumentation comment for this package) ``` diff --git a/sdk/communication/communication-common/src/autoRefreshTokenCredential.ts b/sdk/communication/communication-common/src/autoRefreshTokenCredential.ts index cbeab9a2783a..f5805a7ad6ac 100644 --- a/sdk/communication/communication-common/src/autoRefreshTokenCredential.ts +++ b/sdk/communication/communication-common/src/autoRefreshTokenCredential.ts @@ -24,16 +24,24 @@ export interface CommunicationTokenRefreshOptions { * By default false. */ refreshProactively?: boolean; + + /** + * The time span before token expiry that causes the 'tokenRefresher' to be called if 'refreshProactively' is true. + * For example, setting it to a value equal to 5 minutes means that 5 minutes before the cached token expires, the proactive refresh will request a new token. + * By default, the value is equal to 4.5 minutes. This value ensures that when the 'tokenRefresher' callback is internally using MSAL, MSAL avoids the cache and gets a fresh token. + */ + refreshIntervalBeforeTokenExpiryInSeconds?: number; } const expiredToken = { token: "", expiresOnTimestamp: -10 }; -const minutesToMs = (minutes: number): number => minutes * 1000 * 60; -const defaultRefreshingInterval = minutesToMs(10); +const secondsToMs = (seconds: number): number => seconds * 1000; +const minutesToMs = (minutes: number): number => secondsToMs(minutes * 60); +const defaultRefreshingInterval = minutesToMs(4.5); export class AutoRefreshTokenCredential implements TokenCredential { private readonly refresh: (abortSignal?: AbortSignalLike) => Promise; private readonly refreshProactively: boolean; - private readonly refreshingIntervalInMs: number = defaultRefreshingInterval; + private readonly refreshingIntervalInMs: number; private currentToken: AccessToken; private activeTimeout: ReturnType | undefined; @@ -42,11 +50,20 @@ export class AutoRefreshTokenCredential implements TokenCredential { private disposed = false; constructor(refreshArgs: CommunicationTokenRefreshOptions) { - const { tokenRefresher, token, refreshProactively } = refreshArgs; + const { + tokenRefresher, + token, + refreshProactively, + refreshIntervalBeforeTokenExpiryInSeconds + } = refreshArgs; this.refresh = tokenRefresher; this.currentToken = token ? parseToken(token) : expiredToken; this.refreshProactively = refreshProactively ?? false; + this.refreshingIntervalInMs = + refreshIntervalBeforeTokenExpiryInSeconds !== undefined + ? secondsToMs(refreshIntervalBeforeTokenExpiryInSeconds) + : defaultRefreshingInterval; if (this.refreshProactively) { this.scheduleRefresh(); diff --git a/sdk/communication/communication-common/test/communicationTokenCredential.spec.ts b/sdk/communication/communication-common/test/communicationTokenCredential.spec.ts index 2dd2d14b615e..ca4a9ce84b08 100644 --- a/sdk/communication/communication-common/test/communicationTokenCredential.spec.ts +++ b/sdk/communication/communication-common/test/communicationTokenCredential.spec.ts @@ -111,6 +111,34 @@ describe("CommunicationTokenCredential", () => { sinon.assert.calledOnce(tokenRefresher); }); + it("doesn't proactively refresh a valid token before a custom specified time interval", async () => { + const refreshMinutes = 30; + const tokenValidityMinutes = 60; + const tokenRefresher = sinon.stub().resolves(generateToken(tokenValidityMinutes)); + new AzureCommunicationTokenCredential({ + tokenRefresher, + token: generateToken(tokenValidityMinutes), + refreshProactively: true, + refreshIntervalBeforeTokenExpiryInSeconds: refreshMinutes * 60 + }); + clock.tick((refreshMinutes - 5) * 60 * 1000); + sinon.assert.notCalled(tokenRefresher); + }); + + it("proactively refreshes a valid token after a custom specified time interval", async () => { + const refreshMinutes = 30; + const tokenValidityMinutes = 60; + const tokenRefresher = sinon.stub().resolves(generateToken(tokenValidityMinutes)); + new AzureCommunicationTokenCredential({ + tokenRefresher, + token: generateToken(tokenValidityMinutes), + refreshProactively: true, + refreshIntervalBeforeTokenExpiryInSeconds: refreshMinutes * 60 + }); + clock.tick((refreshMinutes + 5) * 60 * 1000); + sinon.assert.calledOnce(tokenRefresher); + }); + it("returns expired token when not using a lambda", async () => { const token = generateToken(-1); const tokenCredential = new AzureCommunicationTokenCredential(token);