@@ -365,6 +365,23 @@ describe('OAuthProvider', () => {
365
365
expect ( grants . keys . length ) . toBe ( 1 ) ;
366
366
} ) ;
367
367
368
+ it ( 'should reject authorization request with invalid redirect URI' , async ( ) => {
369
+ // Create an authorization request with an invalid redirect URI
370
+ const invalidRedirectUri = 'https://attacker.example.com/callback' ;
371
+ const authRequest = createMockRequest (
372
+ `https://example.com/authorize?response_type=code&client_id=${ clientId } ` +
373
+ `&redirect_uri=${ encodeURIComponent ( invalidRedirectUri ) } ` +
374
+ `&scope=read%20write&state=xyz123`
375
+ ) ;
376
+
377
+ // Expect the request to be rejected
378
+ await expect ( oauthProvider . fetch ( authRequest , mockEnv , mockCtx ) ) . rejects . toThrow ( 'Invalid redirect URI' ) ;
379
+
380
+ // Verify no grant was created
381
+ const grants = await mockEnv . OAUTH_KV . list ( { prefix : 'grant:' } ) ;
382
+ expect ( grants . keys . length ) . toBe ( 0 ) ;
383
+ } ) ;
384
+
368
385
// Add more tests for auth code flow...
369
386
} ) ;
370
387
@@ -694,6 +711,44 @@ describe('OAuthProvider', () => {
694
711
expect ( tokens . expires_in ) . toBe ( 3600 ) ;
695
712
} ) ;
696
713
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
+
697
752
it ( 'should accept the access token for API requests' , async ( ) => {
698
753
// Get an auth code
699
754
const authRequest = createMockRequest (
@@ -2047,6 +2102,135 @@ describe('OAuthProvider', () => {
2047
2102
} ) ;
2048
2103
} ) ;
2049
2104
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
+
2050
2234
describe ( 'Token Revocation' , ( ) => {
2051
2235
let clientId : string ;
2052
2236
let clientSecret : string ;
0 commit comments