diff --git a/spec/unit/oidc/tokenRefresher.spec.ts b/spec/unit/oidc/tokenRefresher.spec.ts index 48c9441612..7c8399c942 100644 --- a/spec/unit/oidc/tokenRefresher.spec.ts +++ b/spec/unit/oidc/tokenRefresher.spec.ts @@ -21,7 +21,6 @@ limitations under the License. import fetchMock from "fetch-mock-jest"; import { OidcTokenRefresher, TokenRefreshLogoutError } from "../../../src"; -import { logger } from "../../../src/logger"; import { makeDelegatedAuthConfig } from "../../test-utils/oidc"; describe("OidcTokenRefresher", () => { @@ -78,51 +77,49 @@ describe("OidcTokenRefresher", () => { fetchMock.resetBehavior(); }); - it("throws when oidc client cannot be initialised", async () => { - jest.spyOn(logger, "error"); - fetchMock.get( - `${config.issuer}.well-known/openid-configuration`, - { - ok: false, - status: 404, - }, - { overwriteRoutes: true }, - ); - const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); - await expect(refresher.oidcClientReady).rejects.toThrow(); - expect(logger.error).toHaveBeenCalledWith( - "Failed to initialise OIDC client.", - // error from OidcClient - expect.any(Error), - ); - }); - - it("initialises oidc client", async () => { - const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); - await refresher.oidcClientReady; - - // @ts-ignore peek at private property to see we initialised the client correctly - expect(refresher.oidcClient.settings).toEqual( - expect.objectContaining({ - client_id: clientId, - redirect_uri: redirectUri, - authority: authConfig.issuer, - scope, - }), - ); - }); - describe("doRefreshAccessToken()", () => { it("should throw when oidcClient has not been initialised", async () => { + fetchMock.get( + `${config.issuer}.well-known/openid-configuration`, + { + ok: false, + status: 404, + }, + { overwriteRoutes: true }, + ); + + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); + await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client."); + }); + + it("should retry initialisation", async () => { + fetchMock.get( + `${config.issuer}.well-known/openid-configuration`, + { + ok: false, + status: 404, + }, + { overwriteRoutes: true }, + ); + const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); - await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow( - "Cannot get new token before OIDC client is initialised.", + await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow("Failed to initialise OIDC client."); + + // put the successful mock back + fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config, { overwriteRoutes: true }); + + const result = await refresher.doRefreshAccessToken("token"); + + expect(result).toEqual( + expect.objectContaining({ + accessToken: "new-access-token", + refreshToken: "new-refresh-token", + }), ); }); it("should refresh the tokens", async () => { const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); - await refresher.oidcClientReady; const result = await refresher.doRefreshAccessToken("refresh-token"); @@ -140,13 +137,12 @@ describe("OidcTokenRefresher", () => { it("should persist the new tokens", async () => { const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims); - await refresher.oidcClientReady; // spy on our stub - jest.spyOn(refresher, "persistTokens"); + jest.spyOn(refresher as any, "persistTokens"); await refresher.doRefreshAccessToken("refresh-token"); - expect(refresher.persistTokens).toHaveBeenCalledWith( + expect((refresher as any).persistTokens).toHaveBeenCalledWith( expect.objectContaining({ accessToken: "new-access-token", refreshToken: "new-refresh-token", diff --git a/src/oidc/tokenRefresher.ts b/src/oidc/tokenRefresher.ts index 34c75e375d..5b520018f2 100644 --- a/src/oidc/tokenRefresher.ts +++ b/src/oidc/tokenRefresher.ts @@ -30,12 +30,15 @@ import { logger } from "../logger.ts"; */ export class OidcTokenRefresher { /** - * Promise which will complete once the OidcClient has been initialised - * and is ready to start refreshing tokens. - * - * Will reject if the client initialisation fails. + * This is now just a resolved promise and will be removed in a future version. + * Initialisation is done lazily at token refresh time. + * @deprecated Consumers no longer need to wait for this promise. */ public readonly oidcClientReady!: Promise; + + // If there is a initialisation attempt in progress, we keep track of it here. + private initPromise?: Promise; + private oidcClient!: OidcClient; private inflightRefreshRequest?: Promise; @@ -43,26 +46,46 @@ export class OidcTokenRefresher { /** * The OIDC issuer as returned by the /auth_issuer API */ - issuer: string, + private issuer: string, /** * id of this client as registered with the OP */ - clientId: string, + private clientId: string, /** * redirectUri as registered with OP */ - redirectUri: string, + private redirectUri: string, /** * Device ID of current session */ - deviceId: string, + protected deviceId: string, /** * idTokenClaims as returned from authorization grant * used to validate tokens */ private readonly idTokenClaims: IdTokenClaims, ) { - this.oidcClientReady = this.initialiseOidcClient(issuer, clientId, deviceId, redirectUri); + this.oidcClientReady = Promise.resolve(); + } + + /** + * Ensures that the client is initialised. + * @returns Promise that resolves when initialisation is complete + * @throws if initialisation fails + */ + private async ensureInit(): Promise { + if (!this.oidcClient) { + if (this.initPromise) { + return this.initPromise; + } + + this.initPromise = this.initialiseOidcClient(this.issuer, this.clientId, this.deviceId, this.redirectUri); + try { + await this.initPromise; + } finally { + this.initPromise = undefined; + } + } } private async initialiseOidcClient( @@ -98,6 +121,8 @@ export class OidcTokenRefresher { * @throws when token refresh fails */ public async doRefreshAccessToken(refreshToken: string): Promise { + await this.ensureInit(); + if (!this.inflightRefreshRequest) { this.inflightRefreshRequest = this.getNewTokens(refreshToken); } @@ -123,7 +148,7 @@ export class OidcTokenRefresher { * @param tokens.accessToken - new access token * @param tokens.refreshToken - OPTIONAL new refresh token */ - public async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise { + protected async persistTokens(tokens: { accessToken: string; refreshToken?: string }): Promise { // NOOP }