diff --git a/src/create-client.test.ts b/src/create-client.test.ts index e7350b3..6b5e8a9 100644 --- a/src/create-client.test.ts +++ b/src/create-client.test.ts @@ -385,17 +385,26 @@ describe("create-client", () => { refreshScope.done(); }); - it("sends a request for each call", async () => { + it("only issues one request for multiple calls", async () => { const client = await clientWithExpiredAccessToken(); - const { scope: refresh1 } = nockRefresh(); - const { scope: refresh2 } = nockRefresh(); + const { scope: refreshScope } = nockRefresh({ + accessTokenClaims: { + jti: "refreshed-token", + }, + }); - const accessToken1 = client.getAccessToken(); - const accessToken2 = client.getAccessToken(); - await Promise.all([accessToken1, accessToken2]); - refresh1.done(); - refresh2.done(); + const accessToken1 = client.getAccessToken().then((token) => { + expect(getClaims(token).jti).toEqual("refreshed-token"); + }); + const accessToken2 = client.getAccessToken().then((token) => { + expect(getClaims(token).jti).toEqual("refreshed-token"); + }); + const accessToken3 = client.getAccessToken().then((token) => { + expect(getClaims(token).jti).toEqual("refreshed-token"); + }); + await Promise.all([accessToken1, accessToken2, accessToken3]); + refreshScope.done(); }); it("throws an error if the refresh fails", async () => { diff --git a/src/create-client.ts b/src/create-client.ts index cc8b5e5..f9277bd 100644 --- a/src/create-client.ts +++ b/src/create-client.ts @@ -33,7 +33,7 @@ interface RedirectOptions { type State = | { tag: "INITIAL" } - | { tag: "AUTHENTICATING" } + | { tag: "AUTHENTICATING"; response: Promise } | { tag: "AUTHENTICATED" } | { tag: "ERROR" }; @@ -171,7 +171,6 @@ export class Client { const code = url.searchParams.get("code"); const stateParam = url.searchParams.get("state"); const state = stateParam ? JSON.parse(stateParam) : undefined; - this.#state = { tag: "AUTHENTICATING" }; // grab the previously stored code verifier from session storage const codeVerifier = window.sessionStorage.getItem( @@ -181,13 +180,17 @@ export class Client { if (code) { if (codeVerifier) { try { - const authenticationResponse = await authenticateWithCode({ - baseUrl: this.#baseUrl, - clientId: this.#clientId, - code, - codeVerifier, - useCookie: this.#useCookie, - }); + this.#state = { + tag: "AUTHENTICATING", + response: authenticateWithCode({ + baseUrl: this.#baseUrl, + clientId: this.#clientId, + code, + codeVerifier, + useCookie: this.#useCookie, + }), + }; + const authenticationResponse = await this.#state.response; if (authenticationResponse) { this.#state = { tag: "AUTHENTICATED" }; @@ -258,11 +261,31 @@ An authorization_code was supplied for a login which did not originate at the ap } async #refreshSession({ organizationId }: { organizationId?: string } = {}) { + if (this.#state.tag === "AUTHENTICATING") { + await this.#state.response; + return; + } + const beginningState = this.#state; - this.#state = { tag: "AUTHENTICATING" }; + this.#state = { + tag: "AUTHENTICATING", + response: this.#doRefresh({ organizationId, beginningState }), + }; + + await this.#state.response; + return; + } + + async #doRefresh({ + organizationId, + beginningState, + }: { + organizationId?: string; + beginningState: State; + }): Promise { try { - await withLock(REFRESH_LOCK_NAME, async () => { + return await withLock(REFRESH_LOCK_NAME, async () => { if (organizationId) { sessionStorage.setItem( ORGANIZATION_ID_SESSION_STORAGE_KEY, @@ -290,6 +313,7 @@ An authorization_code was supplied for a login which did not originate at the ap this.#state = { tag: "AUTHENTICATED" }; setSessionData(authenticationResponse, { devMode: this.#devMode }); this.#onRefresh && this.#onRefresh(authenticationResponse); + return authenticationResponse; }); } catch (error) { if ( @@ -300,8 +324,7 @@ An authorization_code was supplied for a login which did not originate at the ap // preserving the original state so that we can try again next time this.#state = beginningState; - - return; + throw error; } if (beginningState.tag !== "INITIAL") { diff --git a/src/errors.ts b/src/errors.ts index 2f18160..34a51c3 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,6 @@ export class AuthKitError extends Error {} export class RefreshError extends AuthKitError {} +export class CodeExchangeError extends AuthKitError {} export class LoginRequiredError extends AuthKitError { readonly message: string = "No access token available"; } diff --git a/src/utils/authenticate-with-code.ts b/src/utils/authenticate-with-code.ts index 752de51..5f9f8cb 100644 --- a/src/utils/authenticate-with-code.ts +++ b/src/utils/authenticate-with-code.ts @@ -1,3 +1,4 @@ +import { CodeExchangeError } from "../errors"; import { AuthenticationResponseRaw } from "../interfaces"; import { deserializeAuthenticationResponse } from "../serializers"; @@ -31,7 +32,8 @@ export async function authenticateWithCode( if (response.ok) { const data = (await response.json()) as AuthenticationResponseRaw; return deserializeAuthenticationResponse(data); - } else { - console.log("error", await response.json()); } + + const error = (await response.json()) as any; + throw new CodeExchangeError(error.error_description); }