@@ -198,6 +198,135 @@ describe('OAuthProvider', () => {
198
198
mockEnv . OAUTH_KV . clear ( ) ;
199
199
} ) ;
200
200
201
+ describe ( 'API Route Configuration' , ( ) => {
202
+ it ( 'should support multi-handler configuration with apiHandlers' , async ( ) => {
203
+ // Create handler classes for different API routes
204
+ class UsersApiHandler extends WorkerEntrypoint {
205
+ fetch ( request : Request ) {
206
+ return new Response ( 'Users API response' , { status : 200 } ) ;
207
+ }
208
+ }
209
+
210
+ class DocumentsApiHandler extends WorkerEntrypoint {
211
+ fetch ( request : Request ) {
212
+ return new Response ( 'Documents API response' , { status : 200 } ) ;
213
+ }
214
+ }
215
+
216
+ // Create provider with multi-handler configuration
217
+ const providerWithMultiHandler = new OAuthProvider ( {
218
+ apiHandlers : {
219
+ '/api/users/' : UsersApiHandler ,
220
+ '/api/documents/' : DocumentsApiHandler ,
221
+ } ,
222
+ defaultHandler : testDefaultHandler ,
223
+ authorizeEndpoint : '/authorize' ,
224
+ tokenEndpoint : '/oauth/token' ,
225
+ clientRegistrationEndpoint : '/oauth/register' , // Important for registering clients in the test
226
+ scopesSupported : [ 'read' , 'write' ] ,
227
+ } ) ;
228
+
229
+ // Create a client and get an access token
230
+ const clientData = {
231
+ redirect_uris : [ 'https://client.example.com/callback' ] ,
232
+ client_name : 'Test Client' ,
233
+ token_endpoint_auth_method : 'client_secret_basic' ,
234
+ } ;
235
+
236
+ const registerRequest = createMockRequest (
237
+ 'https://example.com/oauth/register' ,
238
+ 'POST' ,
239
+ { 'Content-Type' : 'application/json' } ,
240
+ JSON . stringify ( clientData )
241
+ ) ;
242
+
243
+ const registerResponse = await providerWithMultiHandler . fetch ( registerRequest , mockEnv , mockCtx ) ;
244
+ const client = await registerResponse . json ( ) ;
245
+ const clientId = client . client_id ;
246
+ const clientSecret = client . client_secret ;
247
+ const redirectUri = 'https://client.example.com/callback' ;
248
+
249
+ // Get an auth code
250
+ const authRequest = createMockRequest (
251
+ `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
252
+ `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
253
+ `&scope=read%20write&state=xyz123`
254
+ ) ;
255
+
256
+ const authResponse = await providerWithMultiHandler . fetch ( authRequest , mockEnv , mockCtx ) ;
257
+ const location = authResponse . headers . get ( 'Location' ) ! ;
258
+ const code = new URL ( location ) . searchParams . get ( 'code' ) ! ;
259
+
260
+ // Exchange for tokens
261
+ const params = new URLSearchParams ( ) ;
262
+ params . append ( 'grant_type' , 'authorization_code' ) ;
263
+ params . append ( 'code' , code ) ;
264
+ params . append ( 'redirect_uri' , redirectUri ) ;
265
+ params . append ( 'client_id' , clientId ) ;
266
+ params . append ( 'client_secret' , clientSecret ) ;
267
+
268
+ const tokenRequest = createMockRequest (
269
+ 'https://example.com/oauth/token' ,
270
+ 'POST' ,
271
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
272
+ params . toString ( )
273
+ ) ;
274
+
275
+ const tokenResponse = await providerWithMultiHandler . fetch ( tokenRequest , mockEnv , mockCtx ) ;
276
+ const tokens = await tokenResponse . json ( ) ;
277
+ const accessToken = tokens . access_token ;
278
+
279
+ // Make requests to different API routes
280
+ const usersApiRequest = createMockRequest ( 'https://example.com/api/users/profile' , 'GET' , {
281
+ Authorization : `Bearer ${ accessToken } ` ,
282
+ } ) ;
283
+
284
+ const documentsApiRequest = createMockRequest ( 'https://example.com/api/documents/list' , 'GET' , {
285
+ Authorization : `Bearer ${ accessToken } ` ,
286
+ } ) ;
287
+
288
+ // Request to Users API should be handled by UsersApiHandler
289
+ const usersResponse = await providerWithMultiHandler . fetch ( usersApiRequest , mockEnv , mockCtx ) ;
290
+ expect ( usersResponse . status ) . toBe ( 200 ) ;
291
+ expect ( await usersResponse . text ( ) ) . toBe ( 'Users API response' ) ;
292
+
293
+ // Request to Documents API should be handled by DocumentsApiHandler
294
+ const documentsResponse = await providerWithMultiHandler . fetch ( documentsApiRequest , mockEnv , mockCtx ) ;
295
+ expect ( documentsResponse . status ) . toBe ( 200 ) ;
296
+ expect ( await documentsResponse . text ( ) ) . toBe ( 'Documents API response' ) ;
297
+ } ) ;
298
+
299
+ it ( 'should throw an error when both single-handler and multi-handler configs are provided' , ( ) => {
300
+ expect ( ( ) => {
301
+ new OAuthProvider ( {
302
+ apiRoute : '/api/' ,
303
+ apiHandler : {
304
+ fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
305
+ } ,
306
+ apiHandlers : {
307
+ '/api/users/' : {
308
+ fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
309
+ } ,
310
+ } ,
311
+ defaultHandler : testDefaultHandler ,
312
+ authorizeEndpoint : '/authorize' ,
313
+ tokenEndpoint : '/oauth/token' ,
314
+ } ) ;
315
+ } ) . toThrow ( 'Cannot use both apiRoute/apiHandler and apiHandlers' ) ;
316
+ } ) ;
317
+
318
+ it ( 'should throw an error when neither single-handler nor multi-handler config is provided' , ( ) => {
319
+ expect ( ( ) => {
320
+ new OAuthProvider ( {
321
+ // Intentionally omitting apiRoute and apiHandler and apiHandlers
322
+ defaultHandler : testDefaultHandler ,
323
+ authorizeEndpoint : '/authorize' ,
324
+ tokenEndpoint : '/oauth/token' ,
325
+ } ) ;
326
+ } ) . toThrow ( 'Must provide either apiRoute + apiHandler OR apiHandlers' ) ;
327
+ } ) ;
328
+ } ) ;
329
+
201
330
describe ( 'OAuth Metadata Discovery' , ( ) => {
202
331
it ( 'should return correct metadata at .well-known/oauth-authorization-server' , async ( ) => {
203
332
const request = createMockRequest ( 'https://example.com/.well-known/oauth-authorization-server' ) ;
@@ -644,6 +773,44 @@ describe('OAuthProvider', () => {
644
773
expect ( error . error_description ) . toBe ( 'redirect_uri is required when not using PKCE' ) ;
645
774
} ) ;
646
775
776
+ it ( 'should reject token exchange with code_verifier when PKCE was not used in authorization' , async ( ) => {
777
+ // First get an auth code WITHOUT using PKCE
778
+ const authRequest = createMockRequest (
779
+ `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
780
+ `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
781
+ `&scope=read%20write&state=xyz123`
782
+ ) ;
783
+
784
+ const authResponse = await oauthProvider . fetch ( authRequest , mockEnv , mockCtx ) ;
785
+ const location = authResponse . headers . get ( 'Location' ) ! ;
786
+ const url = new URL ( location ) ;
787
+ const code = url . searchParams . get ( 'code' ) ! ;
788
+
789
+ // Now exchange the code and incorrectly provide a code_verifier
790
+ const params = new URLSearchParams ( ) ;
791
+ params . append ( 'grant_type' , 'authorization_code' ) ;
792
+ params . append ( 'code' , code ) ;
793
+ params . append ( 'redirect_uri' , redirectUri ) ;
794
+ params . append ( 'client_id' , clientId ) ;
795
+ params . append ( 'client_secret' , clientSecret ) ;
796
+ params . append ( 'code_verifier' , 'some_random_verifier_that_wasnt_used_in_auth' ) ;
797
+
798
+ const tokenRequest = createMockRequest (
799
+ 'https://example.com/oauth/token' ,
800
+ 'POST' ,
801
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
802
+ params . toString ( )
803
+ ) ;
804
+
805
+ const tokenResponse = await oauthProvider . fetch ( tokenRequest , mockEnv , mockCtx ) ;
806
+
807
+ // Should fail because code_verifier is provided but PKCE wasn't used in authorization
808
+ expect ( tokenResponse . status ) . toBe ( 400 ) ;
809
+ const error = await tokenResponse . json ( ) ;
810
+ expect ( error . error ) . toBe ( 'invalid_request' ) ;
811
+ expect ( error . error_description ) . toBe ( 'code_verifier provided for a flow that did not use PKCE' ) ;
812
+ } ) ;
813
+
647
814
// Helper function for PKCE tests
648
815
function generateRandomString ( length : number ) : string {
649
816
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
@@ -711,44 +878,6 @@ describe('OAuthProvider', () => {
711
878
expect ( tokens . expires_in ) . toBe ( 3600 ) ;
712
879
} ) ;
713
880
714
- it ( 'should reject token exchange with code_verifier when PKCE was not used in authorization' , async ( ) => {
715
- // First get an auth code WITHOUT using PKCE
716
- const authRequest = createMockRequest (
717
- `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
718
- `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
719
- `&scope=read%20write&state=xyz123`
720
- ) ;
721
-
722
- const authResponse = await oauthProvider . fetch ( authRequest , mockEnv , mockCtx ) ;
723
- const location = authResponse . headers . get ( 'Location' ) ! ;
724
- const url = new URL ( location ) ;
725
- const code = url . searchParams . get ( 'code' ) ! ;
726
-
727
- // Now exchange the code and incorrectly provide a code_verifier
728
- const params = new URLSearchParams ( ) ;
729
- params . append ( 'grant_type' , 'authorization_code' ) ;
730
- params . append ( 'code' , code ) ;
731
- params . append ( 'redirect_uri' , redirectUri ) ;
732
- params . append ( 'client_id' , clientId ) ;
733
- params . append ( 'client_secret' , clientSecret ) ;
734
- params . append ( 'code_verifier' , 'some_random_verifier_that_wasnt_used_in_auth' ) ;
735
-
736
- const tokenRequest = createMockRequest (
737
- 'https://example.com/oauth/token' ,
738
- 'POST' ,
739
- { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
740
- params . toString ( )
741
- ) ;
742
-
743
- const tokenResponse = await oauthProvider . fetch ( tokenRequest , mockEnv , mockCtx ) ;
744
-
745
- // Should fail because code_verifier is provided but PKCE wasn't used in authorization
746
- expect ( tokenResponse . status ) . toBe ( 400 ) ;
747
- const error = await tokenResponse . json ( ) ;
748
- expect ( error . error ) . toBe ( 'invalid_request' ) ;
749
- expect ( error . error_description ) . toBe ( 'code_verifier provided for a flow that did not use PKCE' ) ;
750
- } ) ;
751
-
752
881
it ( 'should accept the access token for API requests' , async ( ) => {
753
882
// Get an auth code
754
883
const authRequest = createMockRequest (
@@ -2102,135 +2231,6 @@ describe('OAuthProvider', () => {
2102
2231
} ) ;
2103
2232
} ) ;
2104
2233
2105
- describe ( 'API Route Configuration' , ( ) => {
2106
- it ( 'should support multi-handler configuration with apiHandlers' , async ( ) => {
2107
- // Create handler classes for different API routes
2108
- class UsersApiHandler extends WorkerEntrypoint {
2109
- fetch ( request : Request ) {
2110
- return new Response ( 'Users API response' , { status : 200 } ) ;
2111
- }
2112
- }
2113
-
2114
- class DocumentsApiHandler extends WorkerEntrypoint {
2115
- fetch ( request : Request ) {
2116
- return new Response ( 'Documents API response' , { status : 200 } ) ;
2117
- }
2118
- }
2119
-
2120
- // Create provider with multi-handler configuration
2121
- const providerWithMultiHandler = new OAuthProvider ( {
2122
- apiHandlers : {
2123
- '/api/users/' : UsersApiHandler ,
2124
- '/api/documents/' : DocumentsApiHandler ,
2125
- } ,
2126
- defaultHandler : testDefaultHandler ,
2127
- authorizeEndpoint : '/authorize' ,
2128
- tokenEndpoint : '/oauth/token' ,
2129
- clientRegistrationEndpoint : '/oauth/register' , // Important for registering clients in the test
2130
- scopesSupported : [ 'read' , 'write' ] ,
2131
- } ) ;
2132
-
2133
- // Create a client and get an access token
2134
- const clientData = {
2135
- redirect_uris : [ 'https://client.example.com/callback' ] ,
2136
- client_name : 'Test Client' ,
2137
- token_endpoint_auth_method : 'client_secret_basic' ,
2138
- } ;
2139
-
2140
- const registerRequest = createMockRequest (
2141
- 'https://example.com/oauth/register' ,
2142
- 'POST' ,
2143
- { 'Content-Type' : 'application/json' } ,
2144
- JSON . stringify ( clientData )
2145
- ) ;
2146
-
2147
- const registerResponse = await providerWithMultiHandler . fetch ( registerRequest , mockEnv , mockCtx ) ;
2148
- const client = await registerResponse . json ( ) ;
2149
- const clientId = client . client_id ;
2150
- const clientSecret = client . client_secret ;
2151
- const redirectUri = 'https://client.example.com/callback' ;
2152
-
2153
- // Get an auth code
2154
- const authRequest = createMockRequest (
2155
- `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
2156
- `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
2157
- `&scope=read%20write&state=xyz123`
2158
- ) ;
2159
-
2160
- const authResponse = await providerWithMultiHandler . fetch ( authRequest , mockEnv , mockCtx ) ;
2161
- const location = authResponse . headers . get ( 'Location' ) ! ;
2162
- const code = new URL ( location ) . searchParams . get ( 'code' ) ! ;
2163
-
2164
- // Exchange for tokens
2165
- const params = new URLSearchParams ( ) ;
2166
- params . append ( 'grant_type' , 'authorization_code' ) ;
2167
- params . append ( 'code' , code ) ;
2168
- params . append ( 'redirect_uri' , redirectUri ) ;
2169
- params . append ( 'client_id' , clientId ) ;
2170
- params . append ( 'client_secret' , clientSecret ) ;
2171
-
2172
- const tokenRequest = createMockRequest (
2173
- 'https://example.com/oauth/token' ,
2174
- 'POST' ,
2175
- { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
2176
- params . toString ( )
2177
- ) ;
2178
-
2179
- const tokenResponse = await providerWithMultiHandler . fetch ( tokenRequest , mockEnv , mockCtx ) ;
2180
- const tokens = await tokenResponse . json ( ) ;
2181
- const accessToken = tokens . access_token ;
2182
-
2183
- // Make requests to different API routes
2184
- const usersApiRequest = createMockRequest ( 'https://example.com/api/users/profile' , 'GET' , {
2185
- Authorization : `Bearer ${ accessToken } ` ,
2186
- } ) ;
2187
-
2188
- const documentsApiRequest = createMockRequest ( 'https://example.com/api/documents/list' , 'GET' , {
2189
- Authorization : `Bearer ${ accessToken } ` ,
2190
- } ) ;
2191
-
2192
- // Request to Users API should be handled by UsersApiHandler
2193
- const usersResponse = await providerWithMultiHandler . fetch ( usersApiRequest , mockEnv , mockCtx ) ;
2194
- expect ( usersResponse . status ) . toBe ( 200 ) ;
2195
- expect ( await usersResponse . text ( ) ) . toBe ( 'Users API response' ) ;
2196
-
2197
- // Request to Documents API should be handled by DocumentsApiHandler
2198
- const documentsResponse = await providerWithMultiHandler . fetch ( documentsApiRequest , mockEnv , mockCtx ) ;
2199
- expect ( documentsResponse . status ) . toBe ( 200 ) ;
2200
- expect ( await documentsResponse . text ( ) ) . toBe ( 'Documents API response' ) ;
2201
- } ) ;
2202
-
2203
- it ( 'should throw an error when both single-handler and multi-handler configs are provided' , ( ) => {
2204
- expect ( ( ) => {
2205
- new OAuthProvider ( {
2206
- apiRoute : '/api/' ,
2207
- apiHandler : {
2208
- fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
2209
- } ,
2210
- apiHandlers : {
2211
- '/api/users/' : {
2212
- fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
2213
- } ,
2214
- } ,
2215
- defaultHandler : testDefaultHandler ,
2216
- authorizeEndpoint : '/authorize' ,
2217
- tokenEndpoint : '/oauth/token' ,
2218
- } ) ;
2219
- } ) . toThrow ( 'Cannot use both apiRoute/apiHandler and apiHandlers' ) ;
2220
- } ) ;
2221
-
2222
- it ( 'should throw an error when neither single-handler nor multi-handler config is provided' , ( ) => {
2223
- expect ( ( ) => {
2224
- new OAuthProvider ( {
2225
- // Intentionally omitting apiRoute and apiHandler and apiHandlers
2226
- defaultHandler : testDefaultHandler ,
2227
- authorizeEndpoint : '/authorize' ,
2228
- tokenEndpoint : '/oauth/token' ,
2229
- } ) ;
2230
- } ) . toThrow ( 'Must provide either apiRoute + apiHandler OR apiHandlers' ) ;
2231
- } ) ;
2232
- } ) ;
2233
-
2234
2234
describe ( 'Token Revocation' , ( ) => {
2235
2235
let clientId : string ;
2236
2236
let clientSecret : string ;
@@ -2311,4 +2311,4 @@ describe('OAuthProvider', () => {
2311
2311
expect ( apiResponse . status ) . toBe ( 401 ) ; // Token should no longer work
2312
2312
} ) ;
2313
2313
} ) ;
2314
- } ) ;
2314
+ } ) ;
0 commit comments