@@ -3,19 +3,17 @@ import { ScenarioUrls } from '../../../types.js';
33import { createAuthServer } from './helpers/createAuthServer.js' ;
44import { createServer } from './helpers/createServer.js' ;
55import { ServerLifecycle } from './helpers/serverLifecycle.js' ;
6- import { Request , Response } from 'express' ;
6+ import express , { Request , Response } from 'express' ;
77
8- export class AuthMarchSpecBackcompatScenario implements Scenario {
9- name = 'auth/march-spec -backcompat' ;
8+ export class Auth20250326OAuthMetadataBackcompatScenario implements Scenario {
9+ name = 'auth/2025-03-26-oauth-metadata -backcompat' ;
1010 description =
11- 'Tests March 2024 spec OAuth flow: no PRM (Protected Resource Metadata), OAuth metadata at root location' ;
11+ 'Tests 2025-03-26 spec OAuth flow: no PRM (Protected Resource Metadata), OAuth metadata at root location' ;
1212 private server = new ServerLifecycle ( ) ;
1313 private checks : ConformanceCheck [ ] = [ ] ;
1414
1515 async start ( ) : Promise < ScenarioUrls > {
1616 this . checks = [ ] ;
17- // TODO: I want to move the Auth URL's below a path.
18-
1917 // Legacy server, so we create the auth server endpoints on the
2018 // same URL as the main server (rather than separating AS / RS).
2119 const authApp = createAuthServer ( this . checks , this . server . getUrl , {
@@ -113,3 +111,215 @@ export class AuthMarchSpecBackcompatScenario implements Scenario {
113111 return this . checks ;
114112 }
115113}
114+
115+ export class Auth20250326OEndpointFallbackScenario implements Scenario {
116+ name = 'auth/2025-03-26-oauth-endpoint-fallback' ;
117+ description =
118+ 'Tests OAuth flow with no metadata endpoints, relying on fallback to standard OAuth endpoints at server root (2025-03-26 spec behavior)' ;
119+ private server = new ServerLifecycle ( ) ;
120+ private checks : ConformanceCheck [ ] = [ ] ;
121+
122+ async start ( ) : Promise < ScenarioUrls > {
123+ this . checks = [ ] ;
124+
125+ const app = createServer (
126+ this . checks ,
127+ this . server . getUrl ,
128+ this . server . getUrl ,
129+ { prmPath : null }
130+ ) ;
131+
132+ // needed for /token endpoint
133+ app . use ( express . urlencoded ( { extended : true } ) ) ;
134+
135+ app . get (
136+ '/.well-known/oauth-authorization-server' ,
137+ ( req : Request , res : Response ) => {
138+ this . checks . push ( {
139+ id : 'no-oauth-metadata' ,
140+ name : 'No OAuth Metadata' ,
141+ description :
142+ 'Client attempted to fetch OAuth metadata, but fallback scenario does not provide it' ,
143+ status : 'SUCCESS' ,
144+ timestamp : new Date ( ) . toISOString ( ) ,
145+ details : {
146+ url : req . url ,
147+ path : req . path ,
148+ note : '2025-03-26 spec behavior: fallback to direct endpoints'
149+ }
150+ } ) ;
151+
152+ res . status ( 404 ) . json ( {
153+ error : 'not_found' ,
154+ error_description : 'OAuth metadata not available (fallback behavior)'
155+ } ) ;
156+ }
157+ ) ;
158+
159+ app . get (
160+ '/.well-known/oauth-protected-resource' ,
161+ ( req : Request , res : Response ) => {
162+ this . checks . push ( {
163+ id : 'no-prm-root' ,
164+ name : 'No PRM at Root' ,
165+ description :
166+ 'Client attempted to fetch PRM at root location, but March spec does not have PRM' ,
167+ status : 'SUCCESS' ,
168+ timestamp : new Date ( ) . toISOString ( ) ,
169+ details : {
170+ url : req . url ,
171+ path : req . path ,
172+ note : '2025-03-26 spec behavior: no PRM available'
173+ }
174+ } ) ;
175+
176+ res . status ( 404 ) . json ( {
177+ error : 'not_found' ,
178+ error_description : 'PRM not available (March spec behavior)'
179+ } ) ;
180+ }
181+ ) ;
182+
183+ app . get (
184+ '/.well-known/oauth-protected-resource/mcp' ,
185+ ( req : Request , res : Response ) => {
186+ this . checks . push ( {
187+ id : 'no-prm-path' ,
188+ name : 'No PRM at Path' ,
189+ description :
190+ 'Client attempted to fetch PRM at path-based location, but March spec behavior does not have PRM' ,
191+ status : 'SUCCESS' ,
192+ timestamp : new Date ( ) . toISOString ( ) ,
193+ details : {
194+ url : req . url ,
195+ path : req . path ,
196+ note : '2025-03-26 spec behavior: no PRM available'
197+ }
198+ } ) ;
199+
200+ res . status ( 404 ) . json ( {
201+ error : 'not_found' ,
202+ error_description : 'PRM not available (March spec behavior)'
203+ } ) ;
204+ }
205+ ) ;
206+
207+ app . get ( '/authorize' , ( req : Request , res : Response ) => {
208+ this . checks . push ( {
209+ id : 'authorization-request' ,
210+ name : 'AuthorizationRequest' ,
211+ description : 'Client made authorization request to fallback endpoint' ,
212+ status : 'SUCCESS' ,
213+ timestamp : new Date ( ) . toISOString ( ) ,
214+ specReferences : [
215+ {
216+ id : 'MCP-2025-03-26-Authorization' ,
217+ url : 'https://modelcontextprotocol.io/specification/2025-03-26/authorization'
218+ }
219+ ] ,
220+ details : {
221+ response_type : req . query . response_type ,
222+ client_id : req . query . client_id ,
223+ redirect_uri : req . query . redirect_uri ,
224+ state : req . query . state ,
225+ code_challenge : req . query . code_challenge ? 'present' : 'missing' ,
226+ code_challenge_method : req . query . code_challenge_method
227+ }
228+ } ) ;
229+
230+ const redirectUri = req . query . redirect_uri as string ;
231+ const state = req . query . state as string ;
232+ const redirectUrl = new URL ( redirectUri ) ;
233+ redirectUrl . searchParams . set ( 'code' , 'test-auth-code' ) ;
234+ if ( state ) {
235+ redirectUrl . searchParams . set ( 'state' , state ) ;
236+ }
237+
238+ res . redirect ( redirectUrl . toString ( ) ) ;
239+ } ) ;
240+
241+ app . post ( '/token' , ( req : Request , res : Response ) => {
242+ this . checks . push ( {
243+ id : 'token-request' ,
244+ name : 'TokenRequest' ,
245+ description : 'Client requested access token from fallback endpoint' ,
246+ status : 'SUCCESS' ,
247+ timestamp : new Date ( ) . toISOString ( ) ,
248+ specReferences : [
249+ {
250+ id : 'MCP-2025-03-26-Authorization' ,
251+ url : 'https://modelcontextprotocol.io/specification/2025-03-26/authorization'
252+ }
253+ ] ,
254+ details : {
255+ endpoint : '/token' ,
256+ grantType : req . body . grant_type
257+ }
258+ } ) ;
259+
260+ res . json ( {
261+ access_token : 'test-token' ,
262+ token_type : 'Bearer' ,
263+ expires_in : 3600
264+ } ) ;
265+ } ) ;
266+
267+ app . post ( '/register' , ( req : Request , res : Response ) => {
268+ this . checks . push ( {
269+ id : 'client-registration' ,
270+ name : 'ClientRegistration' ,
271+ description :
272+ 'Client registered with authorization server at fallback endpoint' ,
273+ status : 'SUCCESS' ,
274+ timestamp : new Date ( ) . toISOString ( ) ,
275+ specReferences : [
276+ {
277+ id : 'MCP-2025-03-26-Authorization' ,
278+ url : 'https://modelcontextprotocol.io/specification/2025-03-26/authorization'
279+ }
280+ ] ,
281+ details : {
282+ endpoint : '/register' ,
283+ clientName : req . body . client_name
284+ }
285+ } ) ;
286+
287+ res . status ( 201 ) . json ( {
288+ client_id : 'test-client-id' ,
289+ client_secret : 'test-client-secret' ,
290+ client_name : req . body . client_name || 'test-client' ,
291+ redirect_uris : req . body . redirect_uris || [ ]
292+ } ) ;
293+ } ) ;
294+
295+ await this . server . start ( app ) ;
296+
297+ return { serverUrl : `${ this . server . getUrl ( ) } /mcp` } ;
298+ }
299+
300+ async stop ( ) {
301+ await this . server . stop ( ) ;
302+ }
303+
304+ getChecks ( ) : ConformanceCheck [ ] {
305+ const expectedSlugs = [
306+ 'client-registration' ,
307+ 'authorization-request' ,
308+ 'token-request'
309+ ] ;
310+
311+ for ( const slug of expectedSlugs ) {
312+ if ( ! this . checks . find ( ( c ) => c . id === slug ) ) {
313+ this . checks . push ( {
314+ id : slug ,
315+ name : `Expected Check Missing: ${ slug } ` ,
316+ description : `Expected Check Missing: ${ slug } ` ,
317+ status : 'FAILURE' ,
318+ timestamp : new Date ( ) . toISOString ( )
319+ } ) ;
320+ }
321+ }
322+
323+ return this . checks ;
324+ }
325+ }
0 commit comments