Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/clients/typescript/auth-test-wif-expired-assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

/**
* Broken WIF client: presents a JWT that is already expired.
* BUG: Uses expired_jwt instead of valid_jwt — server rejects with invalid_grant.
*/

import { runWifJwtBearerExpiredAssertion } from './everything-client.js';
import { runAsCli } from './helpers/cliRunner.js';

runAsCli(
runWifJwtBearerExpiredAssertion,
import.meta.url,
'auth-test-wif-expired-assertion <server-url>'
);
15 changes: 15 additions & 0 deletions examples/clients/typescript/auth-test-wif-grant-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

/**
* Broken WIF client: falls back to authorization_code after receiving unauthorized_client.
* BUG: switches grant type instead of surfacing the error.
*/

import { runWifJwtBearerGrantFallback } from './everything-client.js';
import { runAsCli } from './helpers/cliRunner.js';

runAsCli(
runWifJwtBearerGrantFallback,
import.meta.url,
'auth-test-wif-grant-fallback <server-url>'
);
15 changes: 15 additions & 0 deletions examples/clients/typescript/auth-test-wif-no-assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

/**
* Broken WIF client: omits the assertion parameter from the token request.
* BUG: Does not include assertion in JWT-bearer grant — server rejects with invalid_request.
*/

import { runWifJwtBearerMissingAssertion } from './everything-client.js';
import { runAsCli } from './helpers/cliRunner.js';

runAsCli(
runWifJwtBearerMissingAssertion,
import.meta.url,
'auth-test-wif-no-assertion <server-url>'
);
15 changes: 15 additions & 0 deletions examples/clients/typescript/auth-test-wif-scope-rejected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

/**
* Broken WIF client: requests a scope the AS does not permit for JWT-bearer grant.
* BUG: Includes 'wif.rejected' in the scope parameter — AS returns invalid_scope.
*/

import { runWifJwtBearerScopeRejected } from './everything-client.js';
import { runAsCli } from './helpers/cliRunner.js';

runAsCli(
runWifJwtBearerScopeRejected,
import.meta.url,
'auth-test-wif-scope-rejected <server-url>'
);
15 changes: 15 additions & 0 deletions examples/clients/typescript/auth-test-wif-wrong-audience.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node

/**
* Broken WIF client: presents a JWT with the wrong audience.
* BUG: Uses wrong_audience_jwt instead of valid_jwt — server rejects with invalid_grant.
*/

import { runWifJwtBearerWrongAudience } from './everything-client.js';
import { runAsCli } from './helpers/cliRunner.js';

runAsCli(
runWifJwtBearerWrongAudience,
import.meta.url,
'auth-test-wif-wrong-audience <server-url>'
);
306 changes: 306 additions & 0 deletions examples/clients/typescript/everything-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import {
ClientCredentialsProvider,
PrivateKeyJwtProvider
} from '@modelcontextprotocol/sdk/client/auth-extensions.js';
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
import type {
OAuthClientInformation,
OAuthClientMetadata,
OAuthTokens
} from '@modelcontextprotocol/sdk/shared/auth.js';
import { JWT_BEARER_GRANT_TYPE } from '../../../src/scenarios/client/auth/helpers/createWorkloadJwt.js';
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { ClientConformanceContextSchema } from '../../../src/schemas/context.js';
import {
Expand Down Expand Up @@ -726,6 +733,305 @@ registerScenario(
runEnterpriseManagedAuthorization
);

// ============================================================================
// WIF JWT-bearer scenario
// ============================================================================

class WifJwtBearerProvider implements OAuthClientProvider {
private _tokens?: OAuthTokens;
private _clientInfo: OAuthClientInformation;
private readonly _clientMetadata: OAuthClientMetadata;
private hasAttempted = false;

// Pass null for assertion to deliberately omit it (missing-assertion negative tests).
constructor(
private readonly assertion: string | null,
clientId: string,
private readonly scope?: string
) {
this._clientInfo = { client_id: clientId };
this._clientMetadata = {
client_name: 'conformance-wif-jwt-bearer',
redirect_uris: [],
grant_types: [JWT_BEARER_GRANT_TYPE],
token_endpoint_auth_method: 'none'
};
}

get redirectUrl(): undefined {
return undefined;
}

get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}

clientInformation(): OAuthClientInformation {
return this._clientInfo;
}

saveClientInformation(info: OAuthClientInformation): void {
this._clientInfo = info;
}

tokens(): OAuthTokens | undefined {
return this._tokens;
}

saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}

redirectToAuthorization(): void {
throw new Error('redirectToAuthorization is not used for JWT-bearer flow');
}

saveCodeVerifier(): void {}

codeVerifier(): string {
throw new Error('codeVerifier is not used for JWT-bearer flow');
}

prepareTokenRequest(scope?: string): URLSearchParams {
if (this.hasAttempted) {
throw new Error('JWT-bearer grant must not be retried after failure');
}
this.hasAttempted = true;
const params = new URLSearchParams({ grant_type: JWT_BEARER_GRANT_TYPE });
if (this.assertion !== null) params.set('assertion', this.assertion);
const effectiveScope = this.scope ?? scope;
if (effectiveScope) params.set('scope', effectiveScope);
return params;
}
}

export async function runWifJwtBearer(serverUrl: string): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

const provider = new WifJwtBearerProvider(ctx.valid_jwt, ctx.client_id);

const client = new Client(
{ name: 'conformance-wif-jwt-bearer', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
logger.debug('Successfully connected with JWT-bearer assertion');

await client.listTools();
logger.debug('Successfully listed tools');

await transport.close();
logger.debug('Connection closed successfully');
}

registerScenario('auth/wif-jwt-bearer', runWifJwtBearer);

export async function runWifJwtBearerWrongAudience(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

const provider = new WifJwtBearerProvider(
ctx.wrong_audience_jwt,
ctx.client_id
);

const client = new Client(
{ name: 'conformance-wif-jwt-bearer-wrong-aud', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
await client.listTools();
await transport.close();
}

export async function runWifJwtBearerMissingAssertion(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

// BUG: null omits the assertion parameter from the token request
const provider = new WifJwtBearerProvider(null, ctx.client_id);

const client = new Client(
{ name: 'conformance-wif-no-assertion', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
await client.listTools();
await transport.close();
}

export async function runWifJwtBearerExpiredAssertion(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

const provider = new WifJwtBearerProvider(ctx.expired_jwt, ctx.client_id);

const client = new Client(
{ name: 'conformance-wif-jwt-bearer-expired', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
await client.listTools();
await transport.close();
}

export async function runWifJwtBearerScopeRejected(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

// BUG: requests a scope the AS does not permit for JWT-bearer grant
const provider = new WifJwtBearerProvider(
ctx.valid_jwt,
ctx.client_id,
'wif.rejected'
);

const client = new Client(
{ name: 'conformance-wif-scope-rejected', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
await client.listTools();
await transport.close();
}

// BUG: falls back to authorization_code after receiving unauthorized_client
const WIF_TRIGGER_UNAUTHORIZED_SCOPE = 'wif.trigger-unauthorized';

class WifGrantFallbackProvider implements OAuthClientProvider {
private attemptCount = 0;
private _clientInfo: OAuthClientInformation;
private readonly _clientMetadata: OAuthClientMetadata;

constructor(
private readonly assertion: string,
clientId: string
) {
this._clientInfo = { client_id: clientId };
this._clientMetadata = {
client_name: 'conformance-wif-grant-fallback',
redirect_uris: [],
grant_types: [JWT_BEARER_GRANT_TYPE],
token_endpoint_auth_method: 'none'
};
}

get redirectUrl(): undefined {
return undefined;
}

get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}

clientInformation(): OAuthClientInformation {
return this._clientInfo;
}

saveClientInformation(info: OAuthClientInformation): void {
this._clientInfo = info;
}

tokens(): OAuthTokens | undefined {
return undefined;
}

saveTokens(): void {}

redirectToAuthorization(): void {
throw new Error('redirectToAuthorization is not used for JWT-bearer flow');
}

saveCodeVerifier(): void {}

codeVerifier(): string {
throw new Error('codeVerifier is not used for JWT-bearer flow');
}

prepareTokenRequest(_scope?: string): URLSearchParams {
this.attemptCount++;
if (this.attemptCount === 1) {
const params = new URLSearchParams({ grant_type: JWT_BEARER_GRANT_TYPE });
params.set('assertion', this.assertion);
params.set('scope', WIF_TRIGGER_UNAUTHORIZED_SCOPE);
return params;
}
// BUG: switches to authorization_code instead of surfacing the error
return new URLSearchParams({
grant_type: 'authorization_code',
code: 'fake-fallback-code'
});
}
}

export async function runWifJwtBearerGrantFallback(
serverUrl: string
): Promise<void> {
const ctx = parseContext();
if (ctx.name !== 'auth/wif-jwt-bearer') {
throw new Error(`Expected wif-jwt-bearer context, got ${ctx.name}`);
}

const provider = new WifGrantFallbackProvider(ctx.valid_jwt, ctx.client_id);

const client = new Client(
{ name: 'conformance-wif-grant-fallback', version: '1.0.0' },
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
authProvider: provider
});

await client.connect(transport);
await client.listTools();
await transport.close();
}

// ============================================================================
// Main entry point
// ============================================================================
Expand Down
Loading
Loading