Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit aabb9d8

Browse files
committed
fail fast to logout on invalid refresh token
1 parent f9f844a commit aabb9d8

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

‎packages/core/src/oauth/oauth.test.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ describe("OAuthService.refreshToken", () => {
122122
expect(result.errorCode).toBe("auth_error");
123123
});
124124

125+
it("maps a 400 invalid_grant to an auth_error", async () => {
126+
const { service } = createDeps();
127+
fetchMock.mockResolvedValue(jsonResponse({ error: "invalid_grant" }, 400));
128+
129+
const result = await service.refreshToken("rt", "us");
130+
131+
expect(result.success).toBe(false);
132+
expect(result.errorCode).toBe("auth_error");
133+
});
134+
135+
it("maps a 400 invalid_client to an unknown_error, not a logout", async () => {
136+
const { service } = createDeps();
137+
fetchMock.mockResolvedValue(jsonResponse({ error: "invalid_client" }, 400));
138+
139+
const result = await service.refreshToken("rt", "us");
140+
141+
expect(result.errorCode).toBe("unknown_error");
142+
});
143+
144+
it("maps a 400 with no parseable body to an unknown_error", async () => {
145+
const { service } = createDeps();
146+
fetchMock.mockResolvedValue(new Response("", { status: 400 }));
147+
148+
const result = await service.refreshToken("rt", "us");
149+
150+
expect(result.errorCode).toBe("unknown_error");
151+
});
152+
125153
it("maps 5xx to a server_error", async () => {
126154
const { service } = createDeps();
127155
fetchMock.mockResolvedValue(jsonResponse({}, 503));
@@ -133,7 +161,7 @@ describe("OAuthService.refreshToken", () => {
133161

134162
it("maps other 4xx to an unknown_error", async () => {
135163
const { service } = createDeps();
136-
fetchMock.mockResolvedValue(jsonResponse({}, 400));
164+
fetchMock.mockResolvedValue(jsonResponse({}, 404));
137165

138166
const result = await service.refreshToken("rt", "us");
139167

‎packages/core/src/oauth/oauth.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,20 @@ export class OAuthService {
216216
});
217217

218218
if (!response.ok) {
219-
// 401/403 are auth errors - the token is invalid
220-
const isAuthError = response.status === 401 || response.status === 403;
219+
// 401/403 are always auth failures. A 400 is only a dead refresh token
220+
// when the OAuth error is invalid_grant/invalid_token; other 400s like
221+
// invalid_client or invalid_request are config bugs and must not log the
222+
// user out, or they would be unable to log back in with the same broken
223+
// config.
224+
const oauthError =
225+
response.status === 400
226+
? await this.readOAuthErrorCode(response)
227+
: null;
228+
const isAuthError =
229+
response.status === 401 ||
230+
response.status === 403 ||
231+
oauthError === "invalid_grant" ||
232+
oauthError === "invalid_token";
221233
// 5xx are server errors - should be retried
222234
const isServerError = response.status >= 500;
223235
this.log.warn(
@@ -249,6 +261,15 @@ export class OAuthService {
249261
}
250262
}
251263

264+
private async readOAuthErrorCode(response: Response): Promise<string | null> {
265+
try {
266+
const body = (await response.json()) as { error?: unknown };
267+
return typeof body.error === "string" ? body.error : null;
268+
} catch {
269+
return null;
270+
}
271+
}
272+
252273
/**
253274
* Cancel any pending OAuth flow.
254275
*/

0 commit comments

Comments
 (0)