From 4f55db95c24c974a7cd4e0e3f336faa557cf9bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Mon, 12 May 2025 15:21:03 +0200 Subject: [PATCH] Save existing refresh_token in store if no new refresh_token is returned As described in https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#name-refresh-token-response, server MAY return a new refresh_token. In case it doesn't, current implementation discard initial refresh token if it was still valid. --- src/client/auth.test.ts | 23 +++++++++++++++++++++-- src/client/auth.ts | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index eba7074b..7d88573f 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -342,6 +342,9 @@ describe("OAuth Authorization", () => { access_token: "newaccess123", token_type: "Bearer", expires_in: 3600, + } + const validTokensWithNewRefreshToken = { + ...validTokens, refresh_token: "newrefresh123", }; @@ -356,7 +359,7 @@ describe("OAuth Authorization", () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, - json: async () => validTokens, + json: async () => validTokensWithNewRefreshToken, }); const tokens = await refreshAuthorization("https://auth.example.com", { @@ -364,7 +367,7 @@ describe("OAuth Authorization", () => { refreshToken: "refresh123", }); - expect(tokens).toEqual(validTokens); + expect(tokens).toEqual(validTokensWithNewRefreshToken); expect(mockFetch).toHaveBeenCalledWith( expect.objectContaining({ href: "https://auth.example.com/token", @@ -384,6 +387,22 @@ describe("OAuth Authorization", () => { expect(body.get("client_secret")).toBe("secret123"); }); + it("exchanges refresh token for new tokens and keep existing refresh token if none is returned", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validTokens, + }); + + const refreshToken = "refresh123"; + const tokens = await refreshAuthorization("https://auth.example.com", { + clientInformation: validClientInfo, + refreshToken, + }); + + expect(tokens).toEqual({ refresh_token: refreshToken, ...validTokens }); + }); + it("validates token response schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, diff --git a/src/client/auth.ts b/src/client/auth.ts index b170cefc..5641e601 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -378,7 +378,7 @@ export async function refreshAuthorization( throw new Error(`Token refresh failed: HTTP ${response.status}`); } - return OAuthTokensSchema.parse(await response.json()); + return OAuthTokensSchema.parse({ refresh_token: refreshToken, ...(await response.json()) }); } /**