fix(auth): add issuer to pre-registration context (#423)
This commit is contained in:
parent
c8dab0c3b4
commit
4ff474c20d
4 changed files with 50 additions and 3 deletions
|
|
@ -741,6 +741,10 @@ export async function runPreRegistration(serverUrl: string): Promise<void> {
|
|||
redirect_uris: ['http://localhost:3000/callback']
|
||||
});
|
||||
|
||||
// Associate the pre-registered credentials with the AS that issued them,
|
||||
// keyed by its issuer (Authorization Server Binding).
|
||||
provider.bindIssuer(ctx.issuer);
|
||||
|
||||
// Use the provider-based middleware
|
||||
const oauthFetch = withOAuthRetryWithProvider(
|
||||
provider,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import { runClient as dpopClient } from '../../../../examples/clients/typescript
|
|||
import { getHandler } from '../../../../examples/clients/typescript/everything-client';
|
||||
import { setLogLevel } from '../../../../examples/clients/typescript/helpers/logger';
|
||||
import { DRAFT_PROTOCOL_VERSION } from '../../../types';
|
||||
import { testScenarioContext } from '../../../mock-server/testing';
|
||||
import { ClientConformanceContextSchema } from '../../../schemas/context';
|
||||
|
||||
beforeAll(() => {
|
||||
setLogLevel('error');
|
||||
|
|
@ -112,6 +114,37 @@ describe('Client Draft Scenarios', () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe('auth/pre-registration context', () => {
|
||||
// Authorization Server Binding: the context issuer is only usable as a
|
||||
// binding key if it equals the issuer the mock AS publishes in its metadata.
|
||||
test('supplies the issuer the mock AS publishes in its metadata', async () => {
|
||||
const scenario = authScenariosList.find(
|
||||
(s) => s.name === 'auth/pre-registration'
|
||||
);
|
||||
if (!scenario) {
|
||||
throw new Error('auth/pre-registration scenario not found');
|
||||
}
|
||||
const urls = await scenario.start(testScenarioContext());
|
||||
try {
|
||||
const context = ClientConformanceContextSchema.parse({
|
||||
name: 'auth/pre-registration',
|
||||
...urls.context
|
||||
});
|
||||
if (context.name !== 'auth/pre-registration') {
|
||||
throw new Error(`Unexpected context variant: ${context.name}`);
|
||||
}
|
||||
const res = await fetch(
|
||||
`${context.issuer}/.well-known/oauth-authorization-server`
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
const metadata = (await res.json()) as { issuer?: string };
|
||||
expect(metadata.issuer).toBe(context.issuer);
|
||||
} finally {
|
||||
await scenario.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Negative tests', () => {
|
||||
test('bad client requests root PRM location', async () => {
|
||||
const runner = new InlineClientRunner(badPrmClient);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ const PRE_REGISTERED_CLIENT_SECRET = 'pre-registered-secret';
|
|||
* Scenario: Pre-registration (static client credentials)
|
||||
*
|
||||
* Tests OAuth flow where the server does NOT support Dynamic Client Registration.
|
||||
* Clients must use pre-registered credentials passed via context.
|
||||
* Clients must use pre-registered credentials passed via context. The context
|
||||
* also carries the mock AS's `issuer` identifier: clients targeting the
|
||||
* 2026-07-28 spec MUST associate pre-registered credentials with the
|
||||
* authorization server that issued them, keyed by that issuer
|
||||
* (Authorization Server Binding):
|
||||
* https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#authorization-server-binding
|
||||
*
|
||||
* This tests the pre-registration approach described in the MCP spec:
|
||||
* https://modelcontextprotocol.io/specification/draft/basic/authorization#preregistration
|
||||
|
|
@ -123,7 +128,11 @@ export class PreRegistrationScenario implements Scenario {
|
|||
serverUrl: `${this.server.getUrl()}/mcp`,
|
||||
context: {
|
||||
client_id: PRE_REGISTERED_CLIENT_ID,
|
||||
client_secret: PRE_REGISTERED_CLIENT_SECRET
|
||||
client_secret: PRE_REGISTERED_CLIENT_SECRET,
|
||||
// The `issuer` the mock AS publishes in its metadata — the scenario
|
||||
// sets neither `metadataIssuer` nor `routePrefix`, so createAuthServer
|
||||
// resolves the published issuer to exactly this URL.
|
||||
issuer: this.authServer.getUrl()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ export const ClientConformanceContextSchema = z.discriminatedUnion('name', [
|
|||
z.object({
|
||||
name: z.literal('auth/pre-registration'),
|
||||
client_id: z.string(),
|
||||
client_secret: z.string()
|
||||
client_secret: z.string(),
|
||||
issuer: z.string()
|
||||
}),
|
||||
z.object({
|
||||
name: z.literal('auth/enterprise-managed-authorization'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue