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:
parent
4a2ba8ae7b
commit
67ad358019
1 changed files with 264 additions and 6 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue