@@ -51,6 +51,10 @@ class TokenEndpointAuthScenario implements Scenario {
5151 private server = new ServerLifecycle ( ) ;
5252 private checks : ConformanceCheck [ ] = [ ] ;
5353
54+ // Track resource parameters for RFC 8707 validation
55+ private authorizationResource ?: string ;
56+ private tokenResource ?: string ;
57+
5458 constructor ( expectedAuthMethod : AuthMethod ) {
5559 this . expectedAuthMethod = expectedAuthMethod ;
5660 this . name = `auth/token-endpoint-auth-${ expectedAuthMethod === 'client_secret_basic' ? 'basic' : expectedAuthMethod === 'client_secret_post' ? 'post' : 'none' } ` ;
@@ -59,12 +63,19 @@ class TokenEndpointAuthScenario implements Scenario {
5963
6064 async start ( ) : Promise < ScenarioUrls > {
6165 this . checks = [ ] ;
66+ this . authorizationResource = undefined ;
67+ this . tokenResource = undefined ;
6268 const tokenVerifier = new MockTokenVerifier ( this . checks , [ ] ) ;
6369
6470 const authApp = createAuthServer ( this . checks , this . authServer . getUrl , {
6571 tokenVerifier,
6672 tokenEndpointAuthMethodsSupported : [ this . expectedAuthMethod ] ,
73+ onAuthorizationRequest : ( { resource } ) => {
74+ this . authorizationResource = resource ;
75+ } ,
6776 onTokenRequest : ( { authorizationHeader, body, timestamp } ) => {
77+ // Track resource from token request for RFC 8707 validation
78+ this . tokenResource = body . resource ;
6879 const bodyClientSecret = body . client_secret ;
6980 const actualMethod = detectAuthMethod (
7081 authorizationHeader ,
@@ -145,18 +156,132 @@ class TokenEndpointAuthScenario implements Scenario {
145156 }
146157
147158 getChecks ( ) : ConformanceCheck [ ] {
159+ const timestamp = new Date ( ) . toISOString ( ) ;
160+
148161 if ( ! this . checks . some ( ( c ) => c . id === 'token-endpoint-auth-method' ) ) {
149162 this . checks . push ( {
150163 id : 'token-endpoint-auth-method' ,
151164 name : 'Token endpoint authentication method' ,
152165 description : 'Client did not make a token request' ,
153166 status : 'FAILURE' ,
154- timestamp : new Date ( ) . toISOString ( ) ,
167+ timestamp,
155168 specReferences : [ SpecReferences . OAUTH_2_1_TOKEN ]
156169 } ) ;
157170 }
171+
172+ // RFC 8707 Resource Parameter Validation Checks
173+ this . addResourceParameterChecks ( timestamp ) ;
174+
158175 return this . checks ;
159176 }
177+
178+ private addResourceParameterChecks ( timestamp : string ) : void {
179+ const specRefs = [
180+ SpecReferences . RFC_8707_RESOURCE_INDICATORS ,
181+ SpecReferences . MCP_RESOURCE_PARAMETER
182+ ] ;
183+
184+ // Check 1: Resource parameter in authorization request
185+ if (
186+ ! this . checks . some ( ( c ) => c . id === 'resource-parameter-in-authorization' )
187+ ) {
188+ const hasResource = ! ! this . authorizationResource ;
189+ this . checks . push ( {
190+ id : 'resource-parameter-in-authorization' ,
191+ name : 'Resource parameter in authorization request' ,
192+ description : hasResource
193+ ? 'Client included resource parameter in authorization request'
194+ : 'Client MUST include resource parameter in authorization request per RFC 8707' ,
195+ status : hasResource ? 'SUCCESS' : 'FAILURE' ,
196+ timestamp,
197+ specReferences : specRefs ,
198+ details : {
199+ resource : this . authorizationResource || 'not provided'
200+ }
201+ } ) ;
202+ }
203+
204+ // Check 2: Resource parameter in token request
205+ if ( ! this . checks . some ( ( c ) => c . id === 'resource-parameter-in-token' ) ) {
206+ const hasResource = ! ! this . tokenResource ;
207+ this . checks . push ( {
208+ id : 'resource-parameter-in-token' ,
209+ name : 'Resource parameter in token request' ,
210+ description : hasResource
211+ ? 'Client included resource parameter in token request'
212+ : 'Client MUST include resource parameter in token request per RFC 8707' ,
213+ status : hasResource ? 'SUCCESS' : 'FAILURE' ,
214+ timestamp,
215+ specReferences : specRefs ,
216+ details : {
217+ resource : this . tokenResource || 'not provided'
218+ }
219+ } ) ;
220+ }
221+
222+ // Check 3: Resource parameter is valid canonical URI
223+ if ( ! this . checks . some ( ( c ) => c . id === 'resource-parameter-valid-uri' ) ) {
224+ const resourceToValidate =
225+ this . authorizationResource || this . tokenResource ;
226+ if ( resourceToValidate ) {
227+ const validation = this . validateCanonicalUri ( resourceToValidate ) ;
228+ this . checks . push ( {
229+ id : 'resource-parameter-valid-uri' ,
230+ name : 'Resource parameter is valid canonical URI' ,
231+ description : validation . valid
232+ ? 'Resource parameter is a valid canonical URI (has scheme, no fragment)'
233+ : `Resource parameter is invalid: ${ validation . error } ` ,
234+ status : validation . valid ? 'SUCCESS' : 'FAILURE' ,
235+ timestamp,
236+ specReferences : specRefs ,
237+ details : {
238+ resource : resourceToValidate ,
239+ ...( validation . error && { error : validation . error } )
240+ }
241+ } ) ;
242+ }
243+ }
244+
245+ // Check 4: Resource parameter consistency between requests
246+ if ( ! this . checks . some ( ( c ) => c . id === 'resource-parameter-consistency' ) ) {
247+ if ( this . authorizationResource && this . tokenResource ) {
248+ const consistent = this . authorizationResource === this . tokenResource ;
249+ this . checks . push ( {
250+ id : 'resource-parameter-consistency' ,
251+ name : 'Resource parameter consistency' ,
252+ description : consistent
253+ ? 'Resource parameter is consistent between authorization and token requests'
254+ : 'Resource parameter MUST be consistent between authorization and token requests' ,
255+ status : consistent ? 'SUCCESS' : 'FAILURE' ,
256+ timestamp,
257+ specReferences : specRefs ,
258+ details : {
259+ authorizationResource : this . authorizationResource ,
260+ tokenResource : this . tokenResource
261+ }
262+ } ) ;
263+ }
264+ }
265+ }
266+
267+ private validateCanonicalUri ( uri : string ) : {
268+ valid : boolean ;
269+ error ?: string ;
270+ } {
271+ try {
272+ const parsed = new URL ( uri ) ;
273+ // Check for fragment (RFC 8707: MUST NOT include fragment)
274+ if ( parsed . hash ) {
275+ return {
276+ valid : false ,
277+ error : 'contains fragment (not allowed per RFC 8707)'
278+ } ;
279+ }
280+ return { valid : true } ;
281+ } catch {
282+ return { valid : false , error : 'invalid URI format' } ;
283+ }
284+ }
160285}
161286
162287export class ClientSecretBasicAuthScenario extends TokenEndpointAuthScenario {
0 commit comments