@@ -20,6 +20,9 @@ import { server } from '../setup/msw'
2020
2121const REDIRECT_URI = 'https://app.example.com/cb'
2222const MCP_ORIGIN = 'https://mcp.cloudflare.com'
23+ const MCP_RESOURCE = `${ MCP_ORIGIN } /mcp`
24+ const DOWNSTREAM_CODE_VERIFIER = 'test-downstream-code-verifier'
25+ const DOWNSTREAM_CODE_CHALLENGE = 'I4fhllfHqqQsgap17V2SDI0scSei8H7U0e0rZBDIcbo'
2326
2427/** Register a client via the provider's RFC 7591 endpoint; returns its id. */
2528async function registerClient ( ) : Promise < string > {
@@ -56,6 +59,9 @@ async function beginAuthorization(options: { state?: string; scopes?: string } =
5659 response_type : 'code' ,
5760 client_id : clientId ,
5861 redirect_uri : REDIRECT_URI ,
62+ resource : MCP_RESOURCE ,
63+ code_challenge : DOWNSTREAM_CODE_CHALLENGE ,
64+ code_challenge_method : 'S256' ,
5965 scope : 'user:read' ,
6066 ...( options . state === undefined ? { } : { state : options . state } )
6167 } )
@@ -136,6 +142,29 @@ afterEach(async () => {
136142 await clearKv ( env . OAUTH_KV )
137143} )
138144
145+ describe ( 'OAuth metadata policy' , ( ) => {
146+ it ( 'advertises the canonical MCP endpoint as the protected resource' , async ( ) => {
147+ const response = await exports . default . fetch (
148+ new Request ( `${ MCP_ORIGIN } /.well-known/oauth-protected-resource/mcp` )
149+ )
150+
151+ expect ( response . status ) . toBe ( 200 )
152+ await expect ( response . json ( ) ) . resolves . toMatchObject ( { resource : MCP_RESOURCE } )
153+ } )
154+
155+ it ( 'advertises RFC 9207 authorization response issuer support' , async ( ) => {
156+ const response = await exports . default . fetch (
157+ new Request ( `${ MCP_ORIGIN } /.well-known/oauth-authorization-server` )
158+ )
159+
160+ expect ( response . status ) . toBe ( 200 )
161+ await expect ( response . json ( ) ) . resolves . toMatchObject ( {
162+ issuer : MCP_ORIGIN ,
163+ authorization_response_iss_parameter_supported : true
164+ } )
165+ } )
166+ } )
167+
139168describe ( 'GET /authorize' , ( ) => {
140169 it ( 'renders the consent dialog for a registered client' , async ( ) => {
141170 const clientId = await registerClient ( )
@@ -146,6 +175,9 @@ describe('GET /authorize', () => {
146175 response_type : 'code' ,
147176 client_id : clientId ,
148177 redirect_uri : REDIRECT_URI ,
178+ resource : MCP_RESOURCE ,
179+ code_challenge : DOWNSTREAM_CODE_CHALLENGE ,
180+ code_challenge_method : 'S256' ,
149181 scope : 'user:read'
150182 } )
151183 )
@@ -160,13 +192,37 @@ describe('GET /authorize', () => {
160192 expect ( writtenEvents ( metricsSpy ) ) . not . toContain ( 'auth_user' )
161193 } )
162194
195+ it ( 'rejects a resource other than the canonical MCP endpoint' , async ( ) => {
196+ const clientId = await registerClient ( )
197+ const res = await exports . default . fetch (
198+ new Request (
199+ authorizeUrl ( {
200+ response_type : 'code' ,
201+ client_id : clientId ,
202+ redirect_uri : REDIRECT_URI ,
203+ resource : MCP_ORIGIN ,
204+ code_challenge : DOWNSTREAM_CODE_CHALLENGE ,
205+ code_challenge_method : 'S256'
206+ } )
207+ )
208+ )
209+
210+ expect ( res . status ) . toBe ( 500 )
211+ expect ( await res . text ( ) ) . toContain ( 'Server Error' )
212+ expect ( writtenEvents ( metricsSpy ) ) . toContain ( 'auth_user' )
213+ expect ( ( await env . OAUTH_KV . list ( { prefix : 'grant:' } ) ) . keys ) . toHaveLength ( 0 )
214+ } )
215+
163216 it ( 'logs an auth_user error and 500s for an unknown client' , async ( ) => {
164217 const res = await exports . default . fetch (
165218 new Request (
166219 authorizeUrl ( {
167220 response_type : 'code' ,
168221 client_id : 'does-not-exist' ,
169- redirect_uri : REDIRECT_URI
222+ redirect_uri : REDIRECT_URI ,
223+ resource : MCP_RESOURCE ,
224+ code_challenge : DOWNSTREAM_CODE_CHALLENGE ,
225+ code_challenge_method : 'S256'
170226 } )
171227 )
172228 )
@@ -208,6 +264,7 @@ describe('GET /oauth/callback', () => {
208264 const redirect = new URL ( cbRes . headers . get ( 'location' ) ! )
209265 expect ( redirect . origin + redirect . pathname ) . toBe ( REDIRECT_URI )
210266 expect ( redirect . searchParams . get ( 'code' ) ) . toBeTruthy ( )
267+ expect ( redirect . searchParams . get ( 'iss' ) ) . toBe ( MCP_ORIGIN )
211268
212269 // A successful login records an auth_user datapoint with the userId (blob3)
213270 // and no error message (blob4).
@@ -232,16 +289,28 @@ describe('GET /oauth/callback', () => {
232289 const code = new URL ( callback . headers . get ( 'location' ) ! ) . searchParams . get ( 'code' )
233290 expect ( code ) . toBeTruthy ( )
234291
292+ const tokenParams = {
293+ grant_type : 'authorization_code' ,
294+ code : code ! ,
295+ client_id : clientId ,
296+ redirect_uri : REDIRECT_URI ,
297+ code_verifier : DOWNSTREAM_CODE_VERIFIER
298+ }
299+ const missingResourceResponse = await exports . default . fetch (
300+ new Request ( `${ MCP_ORIGIN } /token` , {
301+ method : 'POST' ,
302+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
303+ body : new URLSearchParams ( tokenParams ) . toString ( )
304+ } )
305+ )
306+ expect ( missingResourceResponse . status ) . toBe ( 400 )
307+ await expect ( missingResourceResponse . json ( ) ) . resolves . toMatchObject ( { error : 'invalid_target' } )
308+
235309 const tokenResponse = await exports . default . fetch (
236310 new Request ( `${ MCP_ORIGIN } /token` , {
237311 method : 'POST' ,
238312 headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
239- body : new URLSearchParams ( {
240- grant_type : 'authorization_code' ,
241- code : code ! ,
242- client_id : clientId ,
243- redirect_uri : REDIRECT_URI
244- } ) . toString ( )
313+ body : new URLSearchParams ( { ...tokenParams , resource : MCP_RESOURCE } ) . toString ( )
245314 } )
246315 )
247316 expect ( tokenResponse . status ) . toBe ( 200 )
0 commit comments