Skip to content

Commit 66de8d8

Browse files
geelenkentonv
authored andcommitted
Validating redirect_uri on authorize
1 parent 3e3d0bf commit 66de8d8

File tree

2 files changed

+71
-38
lines changed

2 files changed

+71
-38
lines changed

__tests__/oauth-provider.test.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,13 @@ describe('OAuthProvider', () => {
206206
return new Response('Users API response', { status: 200 });
207207
}
208208
}
209-
209+
210210
class DocumentsApiHandler extends WorkerEntrypoint {
211211
fetch(request: Request) {
212212
return new Response('Documents API response', { status: 200 });
213213
}
214214
}
215-
215+
216216
// Create provider with multi-handler configuration
217217
const providerWithMultiHandler = new OAuthProvider({
218218
apiHandlers: {
@@ -225,96 +225,96 @@ describe('OAuthProvider', () => {
225225
clientRegistrationEndpoint: '/oauth/register', // Important for registering clients in the test
226226
scopesSupported: ['read', 'write'],
227227
});
228-
228+
229229
// Create a client and get an access token
230230
const clientData = {
231231
redirect_uris: ['https://client.example.com/callback'],
232232
client_name: 'Test Client',
233233
token_endpoint_auth_method: 'client_secret_basic',
234234
};
235-
235+
236236
const registerRequest = createMockRequest(
237237
'https://example.com/oauth/register',
238238
'POST',
239239
{ 'Content-Type': 'application/json' },
240240
JSON.stringify(clientData)
241241
);
242-
242+
243243
const registerResponse = await providerWithMultiHandler.fetch(registerRequest, mockEnv, mockCtx);
244244
const client = await registerResponse.json();
245245
const clientId = client.client_id;
246246
const clientSecret = client.client_secret;
247247
const redirectUri = 'https://client.example.com/callback';
248-
248+
249249
// Get an auth code
250250
const authRequest = createMockRequest(
251251
`https://example.com/authorize?response_type=code&client_id=${clientId}` +
252-
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
253-
`&scope=read%20write&state=xyz123`
252+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
253+
`&scope=read%20write&state=xyz123`
254254
);
255-
255+
256256
const authResponse = await providerWithMultiHandler.fetch(authRequest, mockEnv, mockCtx);
257257
const location = authResponse.headers.get('Location')!;
258258
const code = new URL(location).searchParams.get('code')!;
259-
259+
260260
// Exchange for tokens
261261
const params = new URLSearchParams();
262262
params.append('grant_type', 'authorization_code');
263263
params.append('code', code);
264264
params.append('redirect_uri', redirectUri);
265265
params.append('client_id', clientId);
266266
params.append('client_secret', clientSecret);
267-
267+
268268
const tokenRequest = createMockRequest(
269269
'https://example.com/oauth/token',
270270
'POST',
271271
{ 'Content-Type': 'application/x-www-form-urlencoded' },
272272
params.toString()
273273
);
274-
274+
275275
const tokenResponse = await providerWithMultiHandler.fetch(tokenRequest, mockEnv, mockCtx);
276276
const tokens = await tokenResponse.json();
277277
const accessToken = tokens.access_token;
278-
278+
279279
// Make requests to different API routes
280280
const usersApiRequest = createMockRequest('https://example.com/api/users/profile', 'GET', {
281281
Authorization: `Bearer ${accessToken}`,
282282
});
283-
283+
284284
const documentsApiRequest = createMockRequest('https://example.com/api/documents/list', 'GET', {
285285
Authorization: `Bearer ${accessToken}`,
286286
});
287-
287+
288288
// Request to Users API should be handled by UsersApiHandler
289289
const usersResponse = await providerWithMultiHandler.fetch(usersApiRequest, mockEnv, mockCtx);
290290
expect(usersResponse.status).toBe(200);
291291
expect(await usersResponse.text()).toBe('Users API response');
292-
292+
293293
// Request to Documents API should be handled by DocumentsApiHandler
294294
const documentsResponse = await providerWithMultiHandler.fetch(documentsApiRequest, mockEnv, mockCtx);
295295
expect(documentsResponse.status).toBe(200);
296296
expect(await documentsResponse.text()).toBe('Documents API response');
297297
});
298-
298+
299299
it('should throw an error when both single-handler and multi-handler configs are provided', () => {
300300
expect(() => {
301301
new OAuthProvider({
302302
apiRoute: '/api/',
303303
apiHandler: {
304-
fetch: () => Promise.resolve(new Response())
304+
fetch: () => Promise.resolve(new Response()),
305305
},
306306
apiHandlers: {
307307
'/api/users/': {
308-
fetch: () => Promise.resolve(new Response())
309-
}
308+
fetch: () => Promise.resolve(new Response()),
309+
},
310310
},
311311
defaultHandler: testDefaultHandler,
312312
authorizeEndpoint: '/authorize',
313313
tokenEndpoint: '/oauth/token',
314314
});
315315
}).toThrow('Cannot use both apiRoute/apiHandler and apiHandlers');
316316
});
317-
317+
318318
it('should throw an error when neither single-handler nor multi-handler config is provided', () => {
319319
expect(() => {
320320
new OAuthProvider({
@@ -494,6 +494,23 @@ describe('OAuthProvider', () => {
494494
expect(grants.keys.length).toBe(1);
495495
});
496496

497+
it('should reject authorization request with invalid redirect URI', async () => {
498+
// Create an authorization request with an invalid redirect URI
499+
const invalidRedirectUri = 'https://attacker.example.com/callback';
500+
const authRequest = createMockRequest(
501+
`https://example.com/authorize?response_type=code&client_id=${clientId}` +
502+
`&redirect_uri=${encodeURIComponent(invalidRedirectUri)}` +
503+
`&scope=read%20write&state=xyz123`
504+
);
505+
506+
// Expect the request to be rejected
507+
await expect(oauthProvider.fetch(authRequest, mockEnv, mockCtx)).rejects.toThrow('Invalid redirect URI');
508+
509+
// Verify no grant was created
510+
const grants = await mockEnv.OAUTH_KV.list({ prefix: 'grant:' });
511+
expect(grants.keys.length).toBe(0);
512+
});
513+
497514
// Add more tests for auth code flow...
498515
});
499516

src/oauth-provider.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export interface OAuthProviderOptions {
9999
* URL(s) for API routes. Requests with URLs starting with any of these prefixes
100100
* will be treated as API requests and require a valid access token.
101101
* Can be a single route or an array of routes. Each route can be a full URL or just a path.
102-
*
102+
*
103103
* Used with `apiHandler` for the single-handler configuration. This is incompatible with
104104
* the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
105105
*/
@@ -109,7 +109,7 @@ export interface OAuthProviderOptions {
109109
* Handler for API requests that have a valid access token.
110110
* This handler will receive the authenticated user properties in ctx.props.
111111
* Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
112-
*
112+
*
113113
* Used with `apiRoute` for the single-handler configuration. This is incompatible with
114114
* the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
115115
*/
@@ -120,12 +120,15 @@ export interface OAuthProviderOptions {
120120
* The keys are the API routes (strings only, not arrays), and the values are the handlers.
121121
* Each route can be a full URL or just a path, and each handler can be either an ExportedHandler
122122
* object with a fetch method or a class extending WorkerEntrypoint.
123-
*
123+
*
124124
* This is incompatible with the `apiRoute` and `apiHandler` properties. You must use either
125-
* `apiRoute` + `apiHandler` (single-handler configuration) OR `apiHandlers` (multi-handler
125+
* `apiRoute` + `apiHandler` (single-handler configuration) OR `apiHandlers` (multi-handler
126126
* configuration), not both.
127127
*/
128-
apiHandlers?: Record<string, ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch)>;
128+
apiHandlers?: Record<
129+
string,
130+
ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch)
131+
>;
129132

130133
/**
131134
* Handler for all non-API requests or API requests without a valid token.
@@ -690,7 +693,7 @@ class OAuthProviderImpl {
690693
* Represents the validated type of a handler (ExportedHandler or WorkerEntrypoint)
691694
*/
692695
private typedDefaultHandler: TypedHandler;
693-
696+
694697
/**
695698
* Array of tuples of API routes and their validated handlers
696699
* In the simple case, this will be a single entry with the route and handler from options.apiRoute/apiHandler
@@ -705,33 +708,32 @@ class OAuthProviderImpl {
705708
constructor(options: OAuthProviderOptions) {
706709
// Initialize typedApiHandlers as an array
707710
this.typedApiHandlers = [];
708-
711+
709712
// Check if we have incompatible configuration
710713
const hasSingleHandlerConfig = !!(options.apiRoute && options.apiHandler);
711714
const hasMultiHandlerConfig = !!options.apiHandlers;
712-
715+
713716
if (hasSingleHandlerConfig && hasMultiHandlerConfig) {
714717
throw new TypeError(
715718
'Cannot use both apiRoute/apiHandler and apiHandlers. ' +
716-
'Use either apiRoute + apiHandler OR apiHandlers, not both.'
719+
'Use either apiRoute + apiHandler OR apiHandlers, not both.'
717720
);
718721
}
719-
722+
720723
if (!hasSingleHandlerConfig && !hasMultiHandlerConfig) {
721724
throw new TypeError(
722-
'Must provide either apiRoute + apiHandler OR apiHandlers. ' +
723-
'No API route configuration provided.'
725+
'Must provide either apiRoute + apiHandler OR apiHandlers. ' + 'No API route configuration provided.'
724726
);
725727
}
726-
728+
727729
// Validate default handler
728730
this.typedDefaultHandler = this.validateHandler(options.defaultHandler, 'defaultHandler');
729731

730732
// Process and validate the API handlers
731733
if (hasSingleHandlerConfig) {
732734
// Single handler mode with apiRoute + apiHandler
733735
const apiHandler = this.validateHandler(options.apiHandler!, 'apiHandler');
734-
736+
735737
// For single handler mode, process the apiRoute(s) and map them all to the single apiHandler
736738
if (Array.isArray(options.apiRoute)) {
737739
options.apiRoute.forEach((route, index) => {
@@ -964,7 +966,7 @@ class OAuthProviderImpl {
964966
}
965967
return false;
966968
}
967-
969+
968970
/**
969971
* Finds the appropriate API handler for a URL
970972
* @param url - The URL to find a handler for
@@ -1837,13 +1839,13 @@ class OAuthProviderImpl {
18371839
// Find the appropriate API handler for this URL
18381840
const url = new URL(request.url);
18391841
const apiHandler = this.findApiHandlerForUrl(url);
1840-
1842+
18411843
if (!apiHandler) {
18421844
// This shouldn't happen since we already checked with isApiRequest,
18431845
// but handle it gracefully just in case
18441846
return this.createErrorResponse('invalid_request', 'No handler found for API route', 404);
18451847
}
1846-
1848+
18471849
// Call the API handler based on its type
18481850
if (apiHandler.type === HandlerType.EXPORTED_HANDLER) {
18491851
// It's an object with a fetch method
@@ -2192,6 +2194,20 @@ class OAuthHelpersImpl implements OAuthHelpers {
21922194
throw new Error('The implicit grant flow is not enabled for this provider');
21932195
}
21942196

2197+
// Validate the client ID and redirect URI
2198+
if (clientId) {
2199+
const clientInfo = await this.lookupClient(clientId);
2200+
2201+
// If client exists, validate the redirect URI against registered URIs
2202+
if (clientInfo && redirectUri) {
2203+
if (!clientInfo.redirectUris.includes(redirectUri)) {
2204+
throw new Error(
2205+
`Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`
2206+
);
2207+
}
2208+
}
2209+
}
2210+
21952211
return {
21962212
responseType,
21972213
clientId,

0 commit comments

Comments
 (0)