Skip to content

Commit e50c0f7

Browse files
committed
don't transition to error state for fetch/etc. errors
The ERROR state is meant to indicate terminal errors (like the session being over). authkit-js will not attempt to retrieve new tokens after when in an ERROR state. Temporary errors (like `fetch` or tab-lock errors) should _not_ move to the error state. This allows future calls to `getAccessToken` to attempt to retrieve a new token.
1 parent 22adb78 commit e50c0f7

File tree

2 files changed

+66
-28
lines changed

2 files changed

+66
-28
lines changed

src/create-client.test.ts

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,30 @@ describe("create-client", () => {
372372
});
373373

374374
describe("getAccessToken", () => {
375+
const clientWithExpiredAccessToken = async () => {
376+
const now = Date.now();
377+
const { scope: initialRefreshScope } = nockRefresh({
378+
accessTokenClaims: {
379+
iat: now,
380+
// deliberately issuing a JWT that expires at the same time
381+
// as iat (we don't trust the client's clock, so back-dating
382+
// the timestamps doesn't do anything)
383+
exp: now,
384+
jti: "initial-access-token",
385+
},
386+
});
387+
388+
client = await createClient("client_123abc", {
389+
redirectUri: "https://example.com/",
390+
// turning off auto refresh so we can ensure that we're
391+
// hitting the expired-access token case
392+
onBeforeAutoRefresh: () => false,
393+
});
394+
initialRefreshScope.done();
395+
396+
return client;
397+
};
398+
375399
describe("when the current session is authenticated", () => {
376400
it("returns the access token", async () => {
377401
const { scope } = nockRefresh();
@@ -388,16 +412,18 @@ describe("create-client", () => {
388412

389413
describe("when the current session is not authenticated", () => {
390414
it("throws a `LoginRequiredError`", async () => {
415+
const consoleDebugSpy = jest
416+
.spyOn(console, "debug")
417+
.mockImplementation();
418+
client = await clientWithExpiredAccessToken();
419+
391420
const scope = nock("https://api.workos.com")
392421
.post("/user_management/authenticate", {
393422
client_id: "client_123abc",
394423
grant_type: "refresh_token",
395424
})
396425
.reply(401, {});
397426

398-
client = await createClient("client_123abc", {
399-
redirectUri: "https://example.com/",
400-
});
401427
await expect(client.getAccessToken()).rejects.toThrow(
402428
LoginRequiredError,
403429
);
@@ -406,30 +432,6 @@ describe("create-client", () => {
406432
});
407433

408434
describe("when the current access token has expired", () => {
409-
const clientWithExpiredAccessToken = async () => {
410-
const now = Date.now();
411-
const { scope: initialRefreshScope } = nockRefresh({
412-
accessTokenClaims: {
413-
iat: now,
414-
// deliberately issuing a JWT that expires at the same time
415-
// as iat (we don't trust the client's clock, so back-dating
416-
// the timestamps doesn't do anything)
417-
exp: now,
418-
jti: "initial-access-token",
419-
},
420-
});
421-
422-
client = await createClient("client_123abc", {
423-
redirectUri: "https://example.com/",
424-
// turning off auto refresh so we can ensure that we're
425-
// hitting the expired-access token case
426-
onBeforeAutoRefresh: () => false,
427-
});
428-
initialRefreshScope.done();
429-
430-
return client;
431-
};
432-
433435
it("gets a new access token and returns it", async () => {
434436
const client = await clientWithExpiredAccessToken();
435437

@@ -490,6 +492,34 @@ describe("create-client", () => {
490492

491493
scope.done();
492494
});
495+
496+
it("throws an error if the fetch fails", async () => {
497+
const consoleDebugSpy = jest
498+
.spyOn(console, "debug")
499+
.mockImplementation();
500+
const client = await clientWithExpiredAccessToken();
501+
502+
const errorScope = nock("https://api.workos.com")
503+
.post("/user_management/authenticate", {
504+
client_id: "client_123abc",
505+
grant_type: "refresh_token",
506+
})
507+
.times(2)
508+
.replyWithError(new TypeError("Network Error"));
509+
510+
await expect(client.getAccessToken()).rejects.toThrow(TypeError);
511+
await expect(client.getAccessToken()).rejects.toThrow(TypeError);
512+
expect(consoleDebugSpy.mock.calls).toEqual([
513+
[expect.any(TypeError)],
514+
[expect.any(TypeError)],
515+
]);
516+
errorScope.done();
517+
518+
const { scope: successScope } = nockRefresh();
519+
const accessToken = await client.getAccessToken();
520+
expect(accessToken).toMatch(/^.eyJ/);
521+
successScope.done();
522+
});
493523
});
494524
});
495525

src/create-client.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,16 @@ An authorization_code was supplied for a login which did not originate at the ap
334334
beginningState.tag !== "INITIAL" &&
335335
this.#onRefreshFailure &&
336336
this.#onRefreshFailure({ signIn: this.signIn.bind(this) });
337+
338+
this.#state = { tag: "ERROR" };
339+
} else {
340+
// transitioning into the AUTHENTICATED state ensures that we will
341+
// attempt to refresh the token on future getAccessToken calls()
342+
//
343+
// this could maybe be a new state for clarity? TEMPORARY_ERROR?
344+
this.#state = { tag: "AUTHENTICATED" };
337345
}
338-
this.#state = { tag: "ERROR" };
346+
339347
throw error;
340348
}
341349
}

0 commit comments

Comments
 (0)