feat: add resource parameter validation tests (RFC 8707) (#118)

* feat: add resource parameter validation tests (RFC 8707)

Adds conformance tests for OAuth Resource Indicators (RFC 8707) implementation:

1. Resource parameter checks added to token-endpoint-auth-basic scenario:
   - resource-parameter-in-authorization: Verify resource in auth request
   - resource-parameter-in-token: Verify resource in token request
   - resource-parameter-valid-uri: Verify valid canonical URI
   - resource-parameter-consistency: Verify consistency between requests

2. New auth/resource-mismatch scenario:
   - Tests that client rejects when PRM resource doesn't match server URL
   - Server returns mismatched resource in PRM
   - Test passes if client does NOT proceed with authorization

Also adds spec references for RFC 8707 and MCP resource parameter spec.

Closes #33

* fix: make resource consistency check a FAILURE and remove dead code

- Change resource parameter consistency from WARNING to FAILURE
- Remove unreachable protocol check in validateCanonicalUri
  (URL constructor already validates scheme presence)
This commit is contained in:
Paul Carleton 2026-01-23 18:21:46 +00:00 • committed by GitHub
commit 67ad358019
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -51,6 +51,10 @@ class TokenEndpointAuthScenario implements Scenario {
private server = new ServerLifecycle();
private checks: ConformanceCheck[] = [];
// Track resource parameters for RFC 8707 validation
private authorizationResource?: string;
private tokenResource?: string;
constructor(expectedAuthMethod: AuthMethod) {
this.expectedAuthMethod = expectedAuthMethod;
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 {
async start(): Promise<ScenarioUrls> {
this.checks = [];
this.authorizationResource = undefined;
this.tokenResource = undefined;
const tokenVerifier = new MockTokenVerifier(this.checks, []);
const authApp = createAuthServer(this.checks, this.authServer.getUrl, {
tokenVerifier,
tokenEndpointAuthMethodsSupported: [this.expectedAuthMethod],
onAuthorizationRequest: ({ resource }) => {
this.authorizationResource = resource;
},
onTokenRequest: ({ authorizationHeader, body, timestamp }) => {
// Track resource from token request for RFC 8707 validation
this.tokenResource = body.resource;
const bodyClientSecret = body.client_secret;
const actualMethod = detectAuthMethod(
authorizationHeader,
@ -145,18 +156,132 @@ class TokenEndpointAuthScenario implements Scenario {
}
getChecks(): ConformanceCheck[] {
const timestamp = new Date().toISOString();
if (!this.checks.some((c) => c.id === 'token-endpoint-auth-method')) {
this.checks.push({
id: 'token-endpoint-auth-method',
name: 'Token endpoint authentication method',
description: 'Client did not make a token request',
status: 'FAILURE',
timestamp: new Date().toISOString(),
timestamp,
specReferences: [SpecReferences.OAUTH_2_1_TOKEN]
});
}
// RFC 8707 Resource Parameter Validation Checks
this.addResourceParameterChecks(timestamp);
return this.checks;
}
private addResourceParameterChecks(timestamp: string): void {
const specRefs = [
SpecReferences.RFC_8707_RESOURCE_INDICATORS,
SpecReferences.MCP_RESOURCE_PARAMETER
];
// Check 1: Resource parameter in authorization request
if (
!this.checks.some((c) => c.id === 'resource-parameter-in-authorization')
) {
const hasResource = !!this.authorizationResource;
this.checks.push({
id: 'resource-parameter-in-authorization',
name: 'Resource parameter in authorization request',
description: hasResource
? 'Client included resource parameter in authorization request'
: 'Client MUST include resource parameter in authorization request per RFC 8707',
status: hasResource ? 'SUCCESS' : 'FAILURE',
timestamp,
specReferences: specRefs,
details: {
resource: this.authorizationResource || 'not provided'
}
});
}
// Check 2: Resource parameter in token request
if (!this.checks.some((c) => c.id === 'resource-parameter-in-token')) {
const hasResource = !!this.tokenResource;
this.checks.push({
id: 'resource-parameter-in-token',
name: 'Resource parameter in token request',
description: hasResource
? 'Client included resource parameter in token request'
: 'Client MUST include resource parameter in token request per RFC 8707',
status: hasResource ? 'SUCCESS' : 'FAILURE',
timestamp,
specReferences: specRefs,
details: {
resource: this.tokenResource || 'not provided'
}
});
}
// Check 3: Resource parameter is valid canonical URI
if (!this.checks.some((c) => c.id === 'resource-parameter-valid-uri')) {
const resourceToValidate =
this.authorizationResource || this.tokenResource;
if (resourceToValidate) {
const validation = this.validateCanonicalUri(resourceToValidate);
this.checks.push({
id: 'resource-parameter-valid-uri',
name: 'Resource parameter is valid canonical URI',
description: validation.valid
? 'Resource parameter is a valid canonical URI (has scheme, no fragment)'
: `Resource parameter is invalid: ${validation.error}`,
status: validation.valid ? 'SUCCESS' : 'FAILURE',
timestamp,
specReferences: specRefs,
details: {
resource: resourceToValidate,
...(validation.error && { error: validation.error })
}
});
}
}
// Check 4: Resource parameter consistency between requests
if (!this.checks.some((c) => c.id === 'resource-parameter-consistency')) {
if (this.authorizationResource && this.tokenResource) {
const consistent = this.authorizationResource === this.tokenResource;
this.checks.push({
id: 'resource-parameter-consistency',
name: 'Resource parameter consistency',
description: consistent
? 'Resource parameter is consistent between authorization and token requests'
: 'Resource parameter MUST be consistent between authorization and token requests',
status: consistent ? 'SUCCESS' : 'FAILURE',
timestamp,
specReferences: specRefs,
details: {
authorizationResource: this.authorizationResource,
tokenResource: this.tokenResource
}
});
}
}
}
private validateCanonicalUri(uri: string): {
valid: boolean;
error?: string;
} {
try {
const parsed = new URL(uri);
// Check for fragment (RFC 8707: MUST NOT include fragment)
if (parsed.hash) {
return {
valid: false,
error: 'contains fragment (not allowed per RFC 8707)'
};
}
return { valid: true };
} catch {
return { valid: false, error: 'invalid URI format' };
}
}
}
export class ClientSecretBasicAuthScenario extends TokenEndpointAuthScenario {