|
| 1 | +import type { Scenario, ConformanceCheck } from '../../../types.js'; |
| 2 | +import { ScenarioUrls, SpecVersion } from '../../../types.js'; |
| 3 | +import { createAuthServer } from './helpers/createAuthServer.js'; |
| 4 | +import { createServer } from './helpers/createServer.js'; |
| 5 | +import { ServerLifecycle } from './helpers/serverLifecycle.js'; |
| 6 | +import { SpecReferences } from './spec-references.js'; |
| 7 | + |
| 8 | +/** |
| 9 | + * Scenario: Authorization Server Issuer Mismatch Detection |
| 10 | + * |
| 11 | + * Tests that clients correctly detect and reject when the Authorization |
| 12 | + * Server metadata response contains an `issuer` value that doesn't match |
| 13 | + * the issuer identifier used to construct the metadata URL. |
| 14 | + * |
| 15 | + * Per RFC 8414 §3.3, clients MUST validate that the issuer in the metadata |
| 16 | + * response matches the issuer used to construct the well-known metadata URL. |
| 17 | + * Failing to do so enables mix-up attacks where a malicious AS impersonates |
| 18 | + * another. |
| 19 | + * |
| 20 | + * Setup: |
| 21 | + * - PRM advertises authorization server at http://localhost:<port> (root issuer) |
| 22 | + * - Client constructs metadata URL /.well-known/oauth-authorization-server |
| 23 | + * - AS responds with issuer: "https://evil.example.com" (mismatch) |
| 24 | + * |
| 25 | + * Expected behavior: |
| 26 | + * - Client should NOT proceed with authorization |
| 27 | + * - Client should abort due to issuer mismatch |
| 28 | + * - Test passes if client does NOT make an authorization request |
| 29 | + */ |
| 30 | +export class IssuerMismatchScenario implements Scenario { |
| 31 | + name = 'auth/issuer-mismatch'; |
| 32 | + specVersions: SpecVersion[] = ['draft']; |
| 33 | + description = |
| 34 | + 'Tests that client rejects when AS metadata issuer does not match the issuer used to construct the metadata URL (RFC 8414 §3.3)'; |
| 35 | + allowClientError = true; |
| 36 | + |
| 37 | + private authServer = new ServerLifecycle(); |
| 38 | + private server = new ServerLifecycle(); |
| 39 | + private checks: ConformanceCheck[] = []; |
| 40 | + private authorizationRequestMade = false; |
| 41 | + |
| 42 | + async start(): Promise<ScenarioUrls> { |
| 43 | + this.checks = []; |
| 44 | + this.authorizationRequestMade = false; |
| 45 | + |
| 46 | + const authApp = createAuthServer(this.checks, this.authServer.getUrl, { |
| 47 | + // Root issuer: metadata at /.well-known/oauth-authorization-server, |
| 48 | + // so the expected issuer is just the base URL. Override it to a |
| 49 | + // different origin to trigger the mismatch. |
| 50 | + issuerOverride: 'https://evil.example.com', |
| 51 | + onAuthorizationRequest: () => { |
| 52 | + // If we get here, the client incorrectly proceeded past issuer validation |
| 53 | + this.authorizationRequestMade = true; |
| 54 | + } |
| 55 | + }); |
| 56 | + await this.authServer.start(authApp); |
| 57 | + |
| 58 | + const app = createServer( |
| 59 | + this.checks, |
| 60 | + this.server.getUrl, |
| 61 | + this.authServer.getUrl, |
| 62 | + { |
| 63 | + prmPath: '/.well-known/oauth-protected-resource/mcp' |
| 64 | + } |
| 65 | + ); |
| 66 | + await this.server.start(app); |
| 67 | + |
| 68 | + return { serverUrl: `${this.server.getUrl()}/mcp` }; |
| 69 | + } |
| 70 | + |
| 71 | + async stop() { |
| 72 | + await this.authServer.stop(); |
| 73 | + await this.server.stop(); |
| 74 | + } |
| 75 | + |
| 76 | + getChecks(): ConformanceCheck[] { |
| 77 | + const timestamp = new Date().toISOString(); |
| 78 | + |
| 79 | + if (!this.checks.some((c) => c.id === 'issuer-mismatch-rejected')) { |
| 80 | + const correctlyRejected = !this.authorizationRequestMade; |
| 81 | + this.checks.push({ |
| 82 | + id: 'issuer-mismatch-rejected', |
| 83 | + name: 'Client rejects mismatched issuer', |
| 84 | + description: correctlyRejected |
| 85 | + ? 'Client correctly rejected authorization when AS metadata issuer does not match the metadata URL' |
| 86 | + : 'Client MUST validate that the issuer in AS metadata matches the issuer used to construct the metadata URL (RFC 8414 §3.3)', |
| 87 | + status: correctlyRejected ? 'SUCCESS' : 'FAILURE', |
| 88 | + timestamp, |
| 89 | + specReferences: [SpecReferences.RFC_AUTH_SERVER_METADATA_VALIDATION], |
| 90 | + details: { |
| 91 | + metadataIssuer: 'https://evil.example.com', |
| 92 | + expectedIssuer: this.authServer.getUrl(), |
| 93 | + expectedBehavior: 'Client should NOT proceed with authorization', |
| 94 | + authorizationRequestMade: this.authorizationRequestMade |
| 95 | + } |
| 96 | + }); |
| 97 | + } |
| 98 | + |
| 99 | + return this.checks; |
| 100 | + } |
| 101 | +} |
0 commit comments