From 9b2915e96d4c382f0487d1376d8ab0a3686fcfe8 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 21 May 2025 19:14:22 -0700 Subject: [PATCH 1/7] Allow OAuthClientProvider to control authentication to token endpoint during authorization code exchange. --- src/client/auth.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/client/auth.ts b/src/client/auth.ts index 16f0a550..bcc7719b 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -66,6 +66,8 @@ export interface OAuthClientProvider { * the authorization result. */ codeVerifier(): string | Promise; + + authToTokenEndpoint?(headers: Headers, params: URLSearchParams): void | Promise; } export type AuthResult = "AUTHORIZED" | "REDIRECT"; @@ -131,7 +133,7 @@ export async function auth( // Exchange authorization code for tokens if (authorizationCode !== undefined) { const codeVerifier = await provider.codeVerifier(); - const tokens = await exchangeAuthorization(authorizationServerUrl, { + const tokens = await exchangeAuthorization(authorizationServerUrl, provider, { metadata, clientInformation, authorizationCode, @@ -359,6 +361,7 @@ export async function startAuthorization( */ export async function exchangeAuthorization( authorizationServerUrl: string | URL, + provider: OAuthClientProvider, { metadata, clientInformation, @@ -391,6 +394,9 @@ export async function exchangeAuthorization( tokenUrl = new URL("/token", authorizationServerUrl); } + const headers = new Headers({ + "Content-Type": "application/x-www-form-urlencoded", + }); // Exchange code for tokens const params = new URLSearchParams({ grant_type: grantType, @@ -400,15 +406,15 @@ export async function exchangeAuthorization( redirect_uri: String(redirectUri), }); - if (clientInformation.client_secret) { + if (provider.authToTokenEndpoint) { + provider.authToTokenEndpoint(headers, params); + } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); } const response = await fetch(tokenUrl, { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, + headers: headers, body: params, }); From c7737a8d85013c7159b2cbf2c9ef853e3eb013cf Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 21 May 2025 19:35:45 -0700 Subject: [PATCH 2/7] Include mockProvider in exchangeAuthorization tests. --- src/client/auth.test.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 48be870b..9fc3c828 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -414,6 +414,22 @@ describe("OAuth Authorization", () => { }); describe("exchangeAuthorization", () => { + const mockProvider: OAuthClientProvider = { + get redirectUrl() { return "http://localhost:3000/callback"; }, + get clientMetadata() { + return { + redirect_uris: ["http://localhost:3000/callback"], + client_name: "Test Client", + }; + }, + clientInformation: jest.fn(), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + }; + const validTokens = { access_token: "access123", token_type: "Bearer", @@ -435,7 +451,7 @@ describe("OAuth Authorization", () => { json: async () => validTokens, }); - const tokens = await exchangeAuthorization("https://auth.example.com", { + const tokens = await exchangeAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", @@ -449,9 +465,9 @@ describe("OAuth Authorization", () => { }), expect.objectContaining({ method: "POST", - headers: { + headers: new Headers({ "Content-Type": "application/x-www-form-urlencoded", - }, + }), }) ); @@ -475,7 +491,7 @@ describe("OAuth Authorization", () => { }); await expect( - exchangeAuthorization("https://auth.example.com", { + exchangeAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", @@ -491,7 +507,7 @@ describe("OAuth Authorization", () => { }); await expect( - exchangeAuthorization("https://auth.example.com", { + exchangeAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", From 439a8d3d6430f4364153748141c3633ed1b21337 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 21 May 2025 19:58:02 -0700 Subject: [PATCH 3/7] Add test case for authToTokenEndpoint during exchangeAuthorization. --- src/client/auth.test.ts | 47 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 9fc3c828..7a3bb11b 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -465,12 +465,11 @@ describe("OAuth Authorization", () => { }), expect.objectContaining({ method: "POST", - headers: new Headers({ - "Content-Type": "application/x-www-form-urlencoded", - }), }) ); + const headers = mockFetch.mock.calls[0][1].headers as Headers; + expect(headers.get("Content-Type")).toBe("application/x-www-form-urlencoded"); const body = mockFetch.mock.calls[0][1].body as URLSearchParams; expect(body.get("grant_type")).toBe("authorization_code"); expect(body.get("code")).toBe("code123"); @@ -480,6 +479,48 @@ describe("OAuth Authorization", () => { expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback"); }); + it("exchanges code for tokens with auth", async () => { + mockProvider.authToTokenEndpoint = function(headers: Headers, params: URLSearchParams) { + headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret)); + params.set("example_param", "example_value") + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validTokens, + }); + + const tokens = await exchangeAuthorization("https://auth.example.com", mockProvider, { + clientInformation: validClientInfo, + authorizationCode: "code123", + codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", + }); + + expect(tokens).toEqual(validTokens); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/token", + }), + expect.objectContaining({ + method: "POST", + }) + ); + + const headers = mockFetch.mock.calls[0][1].headers as Headers; + expect(headers.get("Content-Type")).toBe("application/x-www-form-urlencoded"); + expect(headers.get("Authorization")).toBe("Basic Y2xpZW50MTIzOnNlY3JldDEyMw=="); + const body = mockFetch.mock.calls[0][1].body as URLSearchParams; + expect(body.get("grant_type")).toBe("authorization_code"); + expect(body.get("code")).toBe("code123"); + expect(body.get("code_verifier")).toBe("verifier123"); + expect(body.get("client_id")).toBe("client123"); + expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback"); + expect(body.get("example_param")).toBe("example_value"); + expect(body.get("client_secret")).toBeUndefined; + }); + it("validates token response schema", async () => { mockFetch.mockResolvedValueOnce({ ok: true, From 92f90300b4b2eb9df80c9c0c8e8f46fb42188068 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 21 May 2025 20:07:23 -0700 Subject: [PATCH 4/7] Allow OAuthClientProvider to control authentication to token endpoint during refresh token exchange. --- src/client/auth.test.ts | 29 ++++++++++++++++++++++------- src/client/auth.ts | 16 ++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 7a3bb11b..7135aebd 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -559,6 +559,22 @@ describe("OAuth Authorization", () => { }); describe("refreshAuthorization", () => { + const mockProvider: OAuthClientProvider = { + get redirectUrl() { return "http://localhost:3000/callback"; }, + get clientMetadata() { + return { + redirect_uris: ["http://localhost:3000/callback"], + client_name: "Test Client", + }; + }, + clientInformation: jest.fn(), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + }; + const validTokens = { access_token: "newaccess123", token_type: "Bearer", @@ -583,7 +599,7 @@ describe("OAuth Authorization", () => { json: async () => validTokensWithNewRefreshToken, }); - const tokens = await refreshAuthorization("https://auth.example.com", { + const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, refreshToken: "refresh123", }); @@ -595,12 +611,11 @@ describe("OAuth Authorization", () => { }), expect.objectContaining({ method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, }) ); + const headers = mockFetch.mock.calls[0][1].headers as Headers; + expect(headers.get("Content-Type")).toBe("application/x-www-form-urlencoded"); const body = mockFetch.mock.calls[0][1].body as URLSearchParams; expect(body.get("grant_type")).toBe("refresh_token"); expect(body.get("refresh_token")).toBe("refresh123"); @@ -616,7 +631,7 @@ describe("OAuth Authorization", () => { }); const refreshToken = "refresh123"; - const tokens = await refreshAuthorization("https://auth.example.com", { + const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, refreshToken, }); @@ -635,7 +650,7 @@ describe("OAuth Authorization", () => { }); await expect( - refreshAuthorization("https://auth.example.com", { + refreshAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, refreshToken: "refresh123", }) @@ -649,7 +664,7 @@ describe("OAuth Authorization", () => { }); await expect( - refreshAuthorization("https://auth.example.com", { + refreshAuthorization("https://auth.example.com", mockProvider, { clientInformation: validClientInfo, refreshToken: "refresh123", }) diff --git a/src/client/auth.ts b/src/client/auth.ts index bcc7719b..29f61547 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -151,7 +151,7 @@ export async function auth( if (tokens?.refresh_token) { try { // Attempt to refresh the token - const newTokens = await refreshAuthorization(authorizationServerUrl, { + const newTokens = await refreshAuthorization(authorizationServerUrl, provider, { metadata, clientInformation, refreshToken: tokens.refresh_token, @@ -394,10 +394,10 @@ export async function exchangeAuthorization( tokenUrl = new URL("/token", authorizationServerUrl); } + // Exchange code for tokens const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded", }); - // Exchange code for tokens const params = new URLSearchParams({ grant_type: grantType, client_id: clientInformation.client_id, @@ -430,6 +430,7 @@ export async function exchangeAuthorization( */ export async function refreshAuthorization( authorizationServerUrl: string | URL, + provider: OAuthClientProvider, { metadata, clientInformation, @@ -459,21 +460,24 @@ export async function refreshAuthorization( } // Exchange refresh token + const headers = new Headers({ + "Content-Type": "application/x-www-form-urlencoded", + }); const params = new URLSearchParams({ grant_type: grantType, client_id: clientInformation.client_id, refresh_token: refreshToken, }); - if (clientInformation.client_secret) { + if (provider.authToTokenEndpoint) { + provider.authToTokenEndpoint(headers, params); + } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); } const response = await fetch(tokenUrl, { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, + headers: headers, body: params, }); if (!response.ok) { From 58c2332ffee5e55300cb8081ceffc0062d7fa10d Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 21 May 2025 20:10:47 -0700 Subject: [PATCH 5/7] Add test case for authToTokenEndpoint during refreshAuthorization. --- src/client/auth.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 7135aebd..a8a9448a 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -623,6 +623,44 @@ describe("OAuth Authorization", () => { expect(body.get("client_secret")).toBe("secret123"); }); + it("exchanges refresh token for new tokens with auth", async () => { + mockProvider.authToTokenEndpoint = function(headers: Headers, params: URLSearchParams) { + headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret)); + params.set("example_param", "example_value") + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validTokensWithNewRefreshToken, + }); + + const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { + clientInformation: validClientInfo, + refreshToken: "refresh123", + }); + + expect(tokens).toEqual(validTokensWithNewRefreshToken); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/token", + }), + expect.objectContaining({ + method: "POST", + }) + ); + + const headers = mockFetch.mock.calls[0][1].headers as Headers; + expect(headers.get("Content-Type")).toBe("application/x-www-form-urlencoded"); + expect(headers.get("Authorization")).toBe("Basic Y2xpZW50MTIzOnNlY3JldDEyMw=="); + const body = mockFetch.mock.calls[0][1].body as URLSearchParams; + expect(body.get("grant_type")).toBe("refresh_token"); + expect(body.get("refresh_token")).toBe("refresh123"); + expect(body.get("client_id")).toBe("client123"); + expect(body.get("example_param")).toBe("example_value"); + expect(body.get("client_secret")).toBeUndefined; + }); + it("exchanges refresh token for new tokens and keep existing refresh token if none is returned", async () => { mockFetch.mockResolvedValueOnce({ ok: true, From 9ef072fceb610a202343a4fd3c15aaf8f0855969 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Thu, 22 May 2025 15:22:04 -0700 Subject: [PATCH 6/7] Make provider an optional final argument to exchangeAuthorization and refreshAuthorization to maintain compatibility. --- src/client/auth.test.ts | 22 +++++++++++----------- src/client/auth.ts | 16 ++++++++-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index a8a9448a..82d55909 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -451,7 +451,7 @@ describe("OAuth Authorization", () => { json: async () => validTokens, }); - const tokens = await exchangeAuthorization("https://auth.example.com", mockProvider, { + const tokens = await exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", @@ -491,12 +491,12 @@ describe("OAuth Authorization", () => { json: async () => validTokens, }); - const tokens = await exchangeAuthorization("https://auth.example.com", mockProvider, { + const tokens = await exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", redirectUri: "http://localhost:3000/callback", - }); + }, mockProvider); expect(tokens).toEqual(validTokens); expect(mockFetch).toHaveBeenCalledWith( @@ -532,7 +532,7 @@ describe("OAuth Authorization", () => { }); await expect( - exchangeAuthorization("https://auth.example.com", mockProvider, { + exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", @@ -548,7 +548,7 @@ describe("OAuth Authorization", () => { }); await expect( - exchangeAuthorization("https://auth.example.com", mockProvider, { + exchangeAuthorization("https://auth.example.com", { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", @@ -599,7 +599,7 @@ describe("OAuth Authorization", () => { json: async () => validTokensWithNewRefreshToken, }); - const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { + const tokens = await refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }); @@ -635,10 +635,10 @@ describe("OAuth Authorization", () => { json: async () => validTokensWithNewRefreshToken, }); - const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { + const tokens = await refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", - }); + }, mockProvider); expect(tokens).toEqual(validTokensWithNewRefreshToken); expect(mockFetch).toHaveBeenCalledWith( @@ -669,7 +669,7 @@ describe("OAuth Authorization", () => { }); const refreshToken = "refresh123"; - const tokens = await refreshAuthorization("https://auth.example.com", mockProvider, { + const tokens = await refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken, }); @@ -688,7 +688,7 @@ describe("OAuth Authorization", () => { }); await expect( - refreshAuthorization("https://auth.example.com", mockProvider, { + refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }) @@ -702,7 +702,7 @@ describe("OAuth Authorization", () => { }); await expect( - refreshAuthorization("https://auth.example.com", mockProvider, { + refreshAuthorization("https://auth.example.com", { clientInformation: validClientInfo, refreshToken: "refresh123", }) diff --git a/src/client/auth.ts b/src/client/auth.ts index 29f61547..827f23f7 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -133,13 +133,13 @@ export async function auth( // Exchange authorization code for tokens if (authorizationCode !== undefined) { const codeVerifier = await provider.codeVerifier(); - const tokens = await exchangeAuthorization(authorizationServerUrl, provider, { + const tokens = await exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri: provider.redirectUrl, - }); + }, provider); await provider.saveTokens(tokens); return "AUTHORIZED"; @@ -151,11 +151,11 @@ export async function auth( if (tokens?.refresh_token) { try { // Attempt to refresh the token - const newTokens = await refreshAuthorization(authorizationServerUrl, provider, { + const newTokens = await refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken: tokens.refresh_token, - }); + }, provider); await provider.saveTokens(newTokens); return "AUTHORIZED"; @@ -361,7 +361,6 @@ export async function startAuthorization( */ export async function exchangeAuthorization( authorizationServerUrl: string | URL, - provider: OAuthClientProvider, { metadata, clientInformation, @@ -375,6 +374,7 @@ export async function exchangeAuthorization( codeVerifier: string; redirectUri: string | URL; }, + provider?: OAuthClientProvider ): Promise { const grantType = "authorization_code"; @@ -406,7 +406,7 @@ export async function exchangeAuthorization( redirect_uri: String(redirectUri), }); - if (provider.authToTokenEndpoint) { + if (provider?.authToTokenEndpoint) { provider.authToTokenEndpoint(headers, params); } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); @@ -430,7 +430,6 @@ export async function exchangeAuthorization( */ export async function refreshAuthorization( authorizationServerUrl: string | URL, - provider: OAuthClientProvider, { metadata, clientInformation, @@ -440,6 +439,7 @@ export async function refreshAuthorization( clientInformation: OAuthClientInformation; refreshToken: string; }, + provider?: OAuthClientProvider, ): Promise { const grantType = "refresh_token"; @@ -469,7 +469,7 @@ export async function refreshAuthorization( refresh_token: refreshToken, }); - if (provider.authToTokenEndpoint) { + if (provider?.authToTokenEndpoint) { provider.authToTokenEndpoint(headers, params); } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); From c0e9ef653425937103e52879cda64da2a49079c1 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Sun, 25 May 2025 20:48:23 -0700 Subject: [PATCH 7/7] Add url argument to authToTokenEndpoint. --- src/client/auth.test.ts | 12 ++++++++---- src/client/auth.ts | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 82d55909..0f8b9abf 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -480,9 +480,10 @@ describe("OAuth Authorization", () => { }); it("exchanges code for tokens with auth", async () => { - mockProvider.authToTokenEndpoint = function(headers: Headers, params: URLSearchParams) { + mockProvider.authToTokenEndpoint = function(url: URL, headers: Headers, params: URLSearchParams) { headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret)); - params.set("example_param", "example_value") + params.set("example_url", url.toString()); + params.set("example_param", "example_value"); }; mockFetch.mockResolvedValueOnce({ @@ -517,6 +518,7 @@ describe("OAuth Authorization", () => { expect(body.get("code_verifier")).toBe("verifier123"); expect(body.get("client_id")).toBe("client123"); expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback"); + expect(body.get("example_url")).toBe("https://auth.example.com/token"); expect(body.get("example_param")).toBe("example_value"); expect(body.get("client_secret")).toBeUndefined; }); @@ -624,9 +626,10 @@ describe("OAuth Authorization", () => { }); it("exchanges refresh token for new tokens with auth", async () => { - mockProvider.authToTokenEndpoint = function(headers: Headers, params: URLSearchParams) { + mockProvider.authToTokenEndpoint = function(url: URL, headers: Headers, params: URLSearchParams) { headers.set("Authorization", "Basic " + btoa(validClientInfo.client_id + ":" + validClientInfo.client_secret)); - params.set("example_param", "example_value") + params.set("example_url", url.toString()); + params.set("example_param", "example_value"); }; mockFetch.mockResolvedValueOnce({ @@ -657,6 +660,7 @@ describe("OAuth Authorization", () => { expect(body.get("grant_type")).toBe("refresh_token"); expect(body.get("refresh_token")).toBe("refresh123"); expect(body.get("client_id")).toBe("client123"); + expect(body.get("example_url")).toBe("https://auth.example.com/token"); expect(body.get("example_param")).toBe("example_value"); expect(body.get("client_secret")).toBeUndefined; }); diff --git a/src/client/auth.ts b/src/client/auth.ts index 827f23f7..80a086e3 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -67,7 +67,7 @@ export interface OAuthClientProvider { */ codeVerifier(): string | Promise; - authToTokenEndpoint?(headers: Headers, params: URLSearchParams): void | Promise; + authToTokenEndpoint?(url: URL, headers: Headers, params: URLSearchParams): void | Promise; } export type AuthResult = "AUTHORIZED" | "REDIRECT"; @@ -407,7 +407,7 @@ export async function exchangeAuthorization( }); if (provider?.authToTokenEndpoint) { - provider.authToTokenEndpoint(headers, params); + provider.authToTokenEndpoint(tokenUrl, headers, params); } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); } @@ -470,7 +470,7 @@ export async function refreshAuthorization( }); if (provider?.authToTokenEndpoint) { - provider.authToTokenEndpoint(headers, params); + provider.authToTokenEndpoint(tokenUrl, headers, params); } else if (clientInformation.client_secret) { params.set("client_secret", clientInformation.client_secret); }