Skip to content

Commit 1d860bb

Browse files
fixing ProxyOAuthServerProvider: redirect_uri missing in token request (#519)
1 parent 69dbfac commit 1d860bb

File tree

5 files changed

+107
-5
lines changed

5 files changed

+107
-5
lines changed

src/server/auth/handlers/token.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ describe('Token Handler', () => {
322322
client_secret: 'valid-secret',
323323
grant_type: 'authorization_code',
324324
code: 'valid_code',
325-
code_verifier: 'any_verifier'
325+
code_verifier: 'any_verifier',
326+
redirect_uri: 'https://example.com/callback'
326327
});
327328

328329
expect(response.status).toBe(200);
@@ -342,6 +343,69 @@ describe('Token Handler', () => {
342343
global.fetch = originalFetch;
343344
}
344345
});
346+
347+
it('passes through redirect_uri when using proxy provider', async () => {
348+
const originalFetch = global.fetch;
349+
350+
try {
351+
global.fetch = jest.fn().mockResolvedValue({
352+
ok: true,
353+
json: () => Promise.resolve({
354+
access_token: 'mock_access_token',
355+
token_type: 'bearer',
356+
expires_in: 3600,
357+
refresh_token: 'mock_refresh_token'
358+
})
359+
});
360+
361+
const proxyProvider = new ProxyOAuthServerProvider({
362+
endpoints: {
363+
authorizationUrl: 'https://example.com/authorize',
364+
tokenUrl: 'https://example.com/token'
365+
},
366+
verifyAccessToken: async (token) => ({
367+
token,
368+
clientId: 'valid-client',
369+
scopes: ['read', 'write'],
370+
expiresAt: Date.now() / 1000 + 3600
371+
}),
372+
getClient: async (clientId) => clientId === 'valid-client' ? validClient : undefined
373+
});
374+
375+
const proxyApp = express();
376+
const options: TokenHandlerOptions = { provider: proxyProvider };
377+
proxyApp.use('/token', tokenHandler(options));
378+
379+
const redirectUri = 'https://example.com/callback';
380+
const response = await supertest(proxyApp)
381+
.post('/token')
382+
.type('form')
383+
.send({
384+
client_id: 'valid-client',
385+
client_secret: 'valid-secret',
386+
grant_type: 'authorization_code',
387+
code: 'valid_code',
388+
code_verifier: 'any_verifier',
389+
redirect_uri: redirectUri
390+
});
391+
392+
expect(response.status).toBe(200);
393+
expect(response.body.access_token).toBe('mock_access_token');
394+
395+
expect(global.fetch).toHaveBeenCalledWith(
396+
'https://example.com/token',
397+
expect.objectContaining({
398+
method: 'POST',
399+
headers: {
400+
'Content-Type': 'application/x-www-form-urlencoded'
401+
},
402+
body: expect.stringContaining(`redirect_uri=${encodeURIComponent(redirectUri)}`)
403+
})
404+
);
405+
} finally {
406+
global.fetch = originalFetch;
407+
}
408+
});
345409
});
346410

347411
describe('Refresh token grant', () => {

src/server/auth/handlers/token.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const TokenRequestSchema = z.object({
3131
const AuthorizationCodeGrantSchema = z.object({
3232
code: z.string(),
3333
code_verifier: z.string(),
34+
redirect_uri: z.string().optional(),
3435
});
3536

3637
const RefreshTokenGrantSchema = z.object({
@@ -88,7 +89,7 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
8889
throw new InvalidRequestError(parseResult.error.message);
8990
}
9091

91-
const { code, code_verifier } = parseResult.data;
92+
const { code, code_verifier, redirect_uri } = parseResult.data;
9293

9394
const skipLocalPkceValidation = provider.skipLocalPkceValidation;
9495

@@ -102,7 +103,12 @@ export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHand
102103
}
103104

104105
// Passes the code_verifier to the provider if PKCE validation didn't occur locally
105-
const tokens = await provider.exchangeAuthorizationCode(client, code, skipLocalPkceValidation ? code_verifier : undefined);
106+
const tokens = await provider.exchangeAuthorizationCode(
107+
client,
108+
code,
109+
skipLocalPkceValidation ? code_verifier : undefined,
110+
redirect_uri
111+
);
106112
res.status(200).json(tokens);
107113
break;
108114
}

src/server/auth/provider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ export interface OAuthServerProvider {
3636
/**
3737
* Exchanges an authorization code for an access token.
3838
*/
39-
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, codeVerifier?: string): Promise<OAuthTokens>;
39+
exchangeAuthorizationCode(
40+
client: OAuthClientInformationFull,
41+
authorizationCode: string,
42+
codeVerifier?: string,
43+
redirectUri?: string
44+
): Promise<OAuthTokens>;
4045

4146
/**
4247
* Exchanges a refresh token for an access token.

src/server/auth/providers/proxyProvider.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ describe("Proxy OAuth Server Provider", () => {
142142
expect(tokens).toEqual(mockTokenResponse);
143143
});
144144

145+
it("includes redirect_uri in token request when provided", async () => {
146+
const redirectUri = "https://example.com/callback";
147+
const tokens = await provider.exchangeAuthorizationCode(
148+
validClient,
149+
"test-code",
150+
"test-verifier",
151+
redirectUri
152+
);
153+
154+
expect(global.fetch).toHaveBeenCalledWith(
155+
"https://auth.example.com/token",
156+
expect.objectContaining({
157+
method: "POST",
158+
headers: {
159+
"Content-Type": "application/x-www-form-urlencoded",
160+
},
161+
body: expect.stringContaining(`redirect_uri=${encodeURIComponent(redirectUri)}`)
162+
})
163+
);
164+
expect(tokens).toEqual(mockTokenResponse);
165+
});
166+
145167
it("exchanges refresh token for new tokens", async () => {
146168
const tokens = await provider.exchangeRefreshToken(
147169
validClient,

src/server/auth/providers/proxyProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
151151
async exchangeAuthorizationCode(
152152
client: OAuthClientInformationFull,
153153
authorizationCode: string,
154-
codeVerifier?: string
154+
codeVerifier?: string,
155+
redirectUri?: string
155156
): Promise<OAuthTokens> {
156157
const params = new URLSearchParams({
157158
grant_type: "authorization_code",
@@ -167,6 +168,10 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
167168
params.append("code_verifier", codeVerifier);
168169
}
169170

171+
if (redirectUri) {
172+
params.append("redirect_uri", redirectUri);
173+
}
174+
170175
const response = await fetch(this._endpoints.tokenUrl, {
171176
method: "POST",
172177
headers: {

0 commit comments

Comments
 (0)