@@ -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' ) ;
@@ -679,6 +808,44 @@ describe('OAuthProvider', () => {
679
808
expect ( error . error_description ) . toBe ( 'redirect_uri is required when not using PKCE' ) ;
680
809
} ) ;
681
810
811
+ it ( 'should reject token exchange with code_verifier when PKCE was not used in authorization' , async ( ) => {
812
+ // First get an auth code WITHOUT using PKCE
813
+ const authRequest = createMockRequest (
814
+ `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
815
+ `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
816
+ `&scope=read%20write&state=xyz123`
817
+ ) ;
818
+
819
+ const authResponse = await oauthProvider . fetch ( authRequest , mockEnv , mockCtx ) ;
820
+ const location = authResponse . headers . get ( 'Location' ) ! ;
821
+ const url = new URL ( location ) ;
822
+ const code = url . searchParams . get ( 'code' ) ! ;
823
+
824
+ // Now exchange the code and incorrectly provide a code_verifier
825
+ const params = new URLSearchParams ( ) ;
826
+ params . append ( 'grant_type' , 'authorization_code' ) ;
827
+ params . append ( 'code' , code ) ;
828
+ params . append ( 'redirect_uri' , redirectUri ) ;
829
+ params . append ( 'client_id' , clientId ) ;
830
+ params . append ( 'client_secret' , clientSecret ) ;
831
+ params . append ( 'code_verifier' , 'some_random_verifier_that_wasnt_used_in_auth' ) ;
832
+
833
+ const tokenRequest = createMockRequest (
834
+ 'https://example.com/oauth/token' ,
835
+ 'POST' ,
836
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
837
+ params . toString ( )
838
+ ) ;
839
+
840
+ const tokenResponse = await oauthProvider . fetch ( tokenRequest , mockEnv , mockCtx ) ;
841
+
842
+ // Should fail because code_verifier is provided but PKCE wasn't used in authorization
843
+ expect ( tokenResponse . status ) . toBe ( 400 ) ;
844
+ const error = await tokenResponse . json ( ) ;
845
+ expect ( error . error ) . toBe ( 'invalid_request' ) ;
846
+ expect ( error . error_description ) . toBe ( 'code_verifier provided for a flow that did not use PKCE' ) ;
847
+ } ) ;
848
+
682
849
// Helper function for PKCE tests
683
850
function generateRandomString ( length : number ) : string {
684
851
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ;
@@ -746,44 +913,6 @@ describe('OAuthProvider', () => {
746
913
expect ( tokens . expires_in ) . toBe ( 3600 ) ;
747
914
} ) ;
748
915
749
- it ( 'should reject token exchange with code_verifier when PKCE was not used in authorization' , async ( ) => {
750
- // First get an auth code WITHOUT using PKCE
751
- const authRequest = createMockRequest (
752
- `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
753
- `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
754
- `&scope=read%20write&state=xyz123`
755
- ) ;
756
-
757
- const authResponse = await oauthProvider . fetch ( authRequest , mockEnv , mockCtx ) ;
758
- const location = authResponse . headers . get ( 'Location' ) ! ;
759
- const url = new URL ( location ) ;
760
- const code = url . searchParams . get ( 'code' ) ! ;
761
-
762
- // Now exchange the code and incorrectly provide a code_verifier
763
- const params = new URLSearchParams ( ) ;
764
- params . append ( 'grant_type' , 'authorization_code' ) ;
765
- params . append ( 'code' , code ) ;
766
- params . append ( 'redirect_uri' , redirectUri ) ;
767
- params . append ( 'client_id' , clientId ) ;
768
- params . append ( 'client_secret' , clientSecret ) ;
769
- params . append ( 'code_verifier' , 'some_random_verifier_that_wasnt_used_in_auth' ) ;
770
-
771
- const tokenRequest = createMockRequest (
772
- 'https://example.com/oauth/token' ,
773
- 'POST' ,
774
- { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
775
- params . toString ( )
776
- ) ;
777
-
778
- const tokenResponse = await oauthProvider . fetch ( tokenRequest , mockEnv , mockCtx ) ;
779
-
780
- // Should fail because code_verifier is provided but PKCE wasn't used in authorization
781
- expect ( tokenResponse . status ) . toBe ( 400 ) ;
782
- const error = await tokenResponse . json ( ) ;
783
- expect ( error . error ) . toBe ( 'invalid_request' ) ;
784
- expect ( error . error_description ) . toBe ( 'code_verifier provided for a flow that did not use PKCE' ) ;
785
- } ) ;
786
-
787
916
it ( 'should accept the access token for API requests' , async ( ) => {
788
917
// Get an auth code
789
918
const authRequest = createMockRequest (
@@ -2137,135 +2266,6 @@ describe('OAuthProvider', () => {
2137
2266
} ) ;
2138
2267
} ) ;
2139
2268
2140
- describe ( 'API Route Configuration' , ( ) => {
2141
- it ( 'should support multi-handler configuration with apiHandlers' , async ( ) => {
2142
- // Create handler classes for different API routes
2143
- class UsersApiHandler extends WorkerEntrypoint {
2144
- fetch ( request : Request ) {
2145
- return new Response ( 'Users API response' , { status : 200 } ) ;
2146
- }
2147
- }
2148
-
2149
- class DocumentsApiHandler extends WorkerEntrypoint {
2150
- fetch ( request : Request ) {
2151
- return new Response ( 'Documents API response' , { status : 200 } ) ;
2152
- }
2153
- }
2154
-
2155
- // Create provider with multi-handler configuration
2156
- const providerWithMultiHandler = new OAuthProvider ( {
2157
- apiHandlers : {
2158
- '/api/users/' : UsersApiHandler ,
2159
- '/api/documents/' : DocumentsApiHandler ,
2160
- } ,
2161
- defaultHandler : testDefaultHandler ,
2162
- authorizeEndpoint : '/authorize' ,
2163
- tokenEndpoint : '/oauth/token' ,
2164
- clientRegistrationEndpoint : '/oauth/register' , // Important for registering clients in the test
2165
- scopesSupported : [ 'read' , 'write' ] ,
2166
- } ) ;
2167
-
2168
- // Create a client and get an access token
2169
- const clientData = {
2170
- redirect_uris : [ 'https://client.example.com/callback' ] ,
2171
- client_name : 'Test Client' ,
2172
- token_endpoint_auth_method : 'client_secret_basic' ,
2173
- } ;
2174
-
2175
- const registerRequest = createMockRequest (
2176
- 'https://example.com/oauth/register' ,
2177
- 'POST' ,
2178
- { 'Content-Type' : 'application/json' } ,
2179
- JSON . stringify ( clientData )
2180
- ) ;
2181
-
2182
- const registerResponse = await providerWithMultiHandler . fetch ( registerRequest , mockEnv , mockCtx ) ;
2183
- const client = await registerResponse . json ( ) ;
2184
- const clientId = client . client_id ;
2185
- const clientSecret = client . client_secret ;
2186
- const redirectUri = 'https://client.example.com/callback' ;
2187
-
2188
- // Get an auth code
2189
- const authRequest = createMockRequest (
2190
- `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
2191
- `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
2192
- `&scope=read%20write&state=xyz123`
2193
- ) ;
2194
-
2195
- const authResponse = await providerWithMultiHandler . fetch ( authRequest , mockEnv , mockCtx ) ;
2196
- const location = authResponse . headers . get ( 'Location' ) ! ;
2197
- const code = new URL ( location ) . searchParams . get ( 'code' ) ! ;
2198
-
2199
- // Exchange for tokens
2200
- const params = new URLSearchParams ( ) ;
2201
- params . append ( 'grant_type' , 'authorization_code' ) ;
2202
- params . append ( 'code' , code ) ;
2203
- params . append ( 'redirect_uri' , redirectUri ) ;
2204
- params . append ( 'client_id' , clientId ) ;
2205
- params . append ( 'client_secret' , clientSecret ) ;
2206
-
2207
- const tokenRequest = createMockRequest (
2208
- 'https://example.com/oauth/token' ,
2209
- 'POST' ,
2210
- { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
2211
- params . toString ( )
2212
- ) ;
2213
-
2214
- const tokenResponse = await providerWithMultiHandler . fetch ( tokenRequest , mockEnv , mockCtx ) ;
2215
- const tokens = await tokenResponse . json ( ) ;
2216
- const accessToken = tokens . access_token ;
2217
-
2218
- // Make requests to different API routes
2219
- const usersApiRequest = createMockRequest ( 'https://example.com/api/users/profile' , 'GET' , {
2220
- Authorization : `Bearer ${ accessToken } ` ,
2221
- } ) ;
2222
-
2223
- const documentsApiRequest = createMockRequest ( 'https://example.com/api/documents/list' , 'GET' , {
2224
- Authorization : `Bearer ${ accessToken } ` ,
2225
- } ) ;
2226
-
2227
- // Request to Users API should be handled by UsersApiHandler
2228
- const usersResponse = await providerWithMultiHandler . fetch ( usersApiRequest , mockEnv , mockCtx ) ;
2229
- expect ( usersResponse . status ) . toBe ( 200 ) ;
2230
- expect ( await usersResponse . text ( ) ) . toBe ( 'Users API response' ) ;
2231
-
2232
- // Request to Documents API should be handled by DocumentsApiHandler
2233
- const documentsResponse = await providerWithMultiHandler . fetch ( documentsApiRequest , mockEnv , mockCtx ) ;
2234
- expect ( documentsResponse . status ) . toBe ( 200 ) ;
2235
- expect ( await documentsResponse . text ( ) ) . toBe ( 'Documents API response' ) ;
2236
- } ) ;
2237
-
2238
- it ( 'should throw an error when both single-handler and multi-handler configs are provided' , ( ) => {
2239
- expect ( ( ) => {
2240
- new OAuthProvider ( {
2241
- apiRoute : '/api/' ,
2242
- apiHandler : {
2243
- fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
2244
- } ,
2245
- apiHandlers : {
2246
- '/api/users/' : {
2247
- fetch : ( ) => Promise . resolve ( new Response ( ) ) ,
2248
- } ,
2249
- } ,
2250
- defaultHandler : testDefaultHandler ,
2251
- authorizeEndpoint : '/authorize' ,
2252
- tokenEndpoint : '/oauth/token' ,
2253
- } ) ;
2254
- } ) . toThrow ( 'Cannot use both apiRoute/apiHandler and apiHandlers' ) ;
2255
- } ) ;
2256
-
2257
- it ( 'should throw an error when neither single-handler nor multi-handler config is provided' , ( ) => {
2258
- expect ( ( ) => {
2259
- new OAuthProvider ( {
2260
- // Intentionally omitting apiRoute and apiHandler and apiHandlers
2261
- defaultHandler : testDefaultHandler ,
2262
- authorizeEndpoint : '/authorize' ,
2263
- tokenEndpoint : '/oauth/token' ,
2264
- } ) ;
2265
- } ) . toThrow ( 'Must provide either apiRoute + apiHandler OR apiHandlers' ) ;
2266
- } ) ;
2267
- } ) ;
2268
-
2269
2269
describe ( 'Token Revocation' , ( ) => {
2270
2270
let clientId : string ;
2271
2271
let clientSecret : string ;
@@ -2346,4 +2346,4 @@ describe('OAuthProvider', () => {
2346
2346
expect ( apiResponse . status ) . toBe ( 401 ) ; // Token should no longer work
2347
2347
} ) ;
2348
2348
} ) ;
2349
- } ) ;
2349
+ } ) ;
0 commit comments