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
2 changes: 1 addition & 1 deletion docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Examples:
- [`simpleOAuthClient.ts`](../src/examples/client/simpleOAuthClient.ts)
- [`simpleOAuthClientProvider.ts`](../src/examples/client/simpleOAuthClientProvider.ts)
- [`simpleClientCredentials.ts`](../src/examples/client/simpleClientCredentials.ts)
- Server-side auth demo: [`demoInMemoryOAuthProvider.ts`](../src/examples/server/demoInMemoryOAuthProvider.ts)
- Server-side auth demo: [`demoInMemoryOAuthProvider.ts`](../src/examples/server/demoInMemoryOAuthProvider.ts) (tests live under `test/examples/server/demoInMemoryOAuthProvider.test.ts`)

These examples show how to:

Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,73 +1,16 @@
import { describe, it, expect } from 'vitest';
import { auth } from './auth.js';
import { auth } from '../../src/client/auth.js';
import {
ClientCredentialsProvider,
PrivateKeyJwtProvider,
StaticPrivateKeyJwtProvider,
createPrivateKeyJwtAuth
} from './auth-extensions.js';
import type { FetchLike } from '../shared/transport.js';
} from '../../src/client/auth-extensions.js';
import { createMockOAuthFetch } from '../helpers/oauth.js';

const RESOURCE_SERVER_URL = 'https://resource.example.com/';
const AUTH_SERVER_URL = 'https://auth.example.com';

function createMockFetch(onTokenRequest?: (url: URL, init: RequestInit | undefined) => void | Promise<void>): FetchLike {
return async (input: string | URL, init?: RequestInit): Promise<Response> => {
const url = input instanceof URL ? input : new URL(input);

// Protected resource metadata discovery
if (url.origin === RESOURCE_SERVER_URL.slice(0, -1) && url.pathname === '/.well-known/oauth-protected-resource') {
return new Response(
JSON.stringify({
resource: RESOURCE_SERVER_URL,
authorization_servers: [AUTH_SERVER_URL]
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}

// Authorization server metadata discovery
if (url.origin === AUTH_SERVER_URL && url.pathname === '/.well-known/oauth-authorization-server') {
return new Response(
JSON.stringify({
issuer: AUTH_SERVER_URL,
authorization_endpoint: `${AUTH_SERVER_URL}/authorize`,
token_endpoint: `${AUTH_SERVER_URL}/token`,
response_types_supported: ['code'],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'private_key_jwt']
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}

// Token endpoint
if (url.origin === AUTH_SERVER_URL && url.pathname === '/token') {
if (onTokenRequest) {
await onTokenRequest(url, init);
}

return new Response(
JSON.stringify({
access_token: 'test-access-token',
token_type: 'Bearer'
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}

throw new Error(`Unexpected URL in mock fetch: ${url.toString()}`);
};
}

describe('auth-extensions providers (end-to-end with auth())', () => {
it('authenticates using ClientCredentialsProvider with client_secret_basic', async () => {
const provider = new ClientCredentialsProvider({
Expand All @@ -76,19 +19,23 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
clientName: 'test-client'
});

const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
expect(params.get('client_assertion')).toBeNull();

const headers = new Headers(init?.headers);
const authHeader = headers.get('Authorization');
expect(authHeader).toBeTruthy();

const expectedCredentials = Buffer.from('my-client:my-secret').toString('base64');
expect(authHeader).toBe(`Basic ${expectedCredentials}`);
const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
expect(params.get('client_assertion')).toBeNull();

const headers = new Headers(init?.headers);
const authHeader = headers.get('Authorization');
expect(authHeader).toBeTruthy();

const expectedCredentials = Buffer.from('my-client:my-secret').toString('base64');
expect(authHeader).toBe(`Basic ${expectedCredentials}`);
}
});

const result = await auth(provider, {
Expand All @@ -112,21 +59,25 @@ describe('auth-extensions providers (end-to-end with auth())', () => {

let assertionFromRequest: string | null = null;

const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);

assertionFromRequest = params.get('client_assertion');
expect(assertionFromRequest).toBeTruthy();
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
assertionFromRequest = params.get('client_assertion');
expect(assertionFromRequest).toBeTruthy();
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');

const parts = assertionFromRequest!.split('.');
expect(parts).toHaveLength(3);
const parts = assertionFromRequest!.split('.');
expect(parts).toHaveLength(3);

const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
}
});

const result = await auth(provider, {
Expand All @@ -149,7 +100,10 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
clientName: 'private-key-jwt-client'
});

const fetchMock = createMockFetch();
const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL
});

await expect(
auth(provider, {
Expand All @@ -168,17 +122,21 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
clientName: 'static-private-key-jwt-client'
});

const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);

expect(params.get('client_assertion')).toBe(staticAssertion);
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
expect(params.get('client_assertion')).toBe(staticAssertion);
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');

const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
}
});

const result = await auth(provider, {
Expand Down
16 changes: 8 additions & 8 deletions src/client/auth.test.ts → test/client/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LATEST_PROTOCOL_VERSION } from '../types.js';
import { LATEST_PROTOCOL_VERSION } from '../../src/types.js';
import {
discoverOAuthMetadata,
discoverAuthorizationServerMetadata,
Expand All @@ -13,10 +13,10 @@ import {
type OAuthClientProvider,
selectClientAuthMethod,
isHttpsUrl
} from './auth.js';
import { createPrivateKeyJwtAuth } from './auth-extensions.js';
import { InvalidClientMetadataError, ServerError } from '../server/auth/errors.js';
import { AuthorizationServerMetadata, OAuthTokens } from '../shared/auth.js';
} from '../../src/client/auth.js';
import { createPrivateKeyJwtAuth } from '../../src/client/auth-extensions.js';
import { InvalidClientMetadataError, ServerError } from '../../src/server/auth/errors.js';
import { AuthorizationServerMetadata, OAuthTokens } from '../../src/shared/auth.js';
import { expect, vi, type Mock } from 'vitest';

// Mock pkce-challenge
Expand Down Expand Up @@ -2836,7 +2836,7 @@ describe('OAuth Authorization', () => {

describe('RequestInit headers passthrough', () => {
it('custom headers from RequestInit are passed to auth discovery requests', async () => {
const { createFetchWithInit } = await import('../shared/transport.js');
const { createFetchWithInit } = await import('../../src/shared/transport.js');

const customFetch = vi.fn().mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -2869,7 +2869,7 @@ describe('OAuth Authorization', () => {
});

it('auth-specific headers override base headers from RequestInit', async () => {
const { createFetchWithInit } = await import('../shared/transport.js');
const { createFetchWithInit } = await import('../../src/shared/transport.js');

const customFetch = vi.fn().mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -2907,7 +2907,7 @@ describe('OAuth Authorization', () => {
});

it('other RequestInit options are passed through', async () => {
const { createFetchWithInit } = await import('../shared/transport.js');
const { createFetchWithInit } = await import('../../src/shared/transport.js');

const customFetch = vi.fn().mockResolvedValue({
ok: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StdioClientTransport, getDefaultEnvironment } from './stdio.js';
import { StdioClientTransport, getDefaultEnvironment } from '../../src/client/stdio.js';
import spawn from 'cross-spawn';
import { JSONRPCMessage } from '../types.js';
import { JSONRPCMessage } from '../../src/types.js';
import { ChildProcess } from 'node:child_process';
import { Mock, MockedFunction } from 'vitest';

Expand Down
14 changes: 7 additions & 7 deletions src/client/index.test.ts → test/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-constant-binary-expression */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { Client, getSupportedElicitationModes } from './index.js';
import { Client, getSupportedElicitationModes } from '../../src/client/index.js';
import {
RequestSchema,
NotificationSchema,
Expand All @@ -25,12 +25,12 @@ import {
Tool,
Prompt,
Resource
} from '../types.js';
import { Transport } from '../shared/transport.js';
import { Server } from '../server/index.js';
import { McpServer } from '../server/mcp.js';
import { InMemoryTransport } from '../inMemory.js';
import { InMemoryTaskStore, InMemoryTaskMessageQueue } from '../experimental/tasks/stores/in-memory.js';
} from '../../src/types.js';
import { Transport } from '../../src/shared/transport.js';
import { Server } from '../../src/server/index.js';
import { McpServer } from '../../src/server/mcp.js';
import { InMemoryTransport } from '../../src/inMemory.js';
import { InMemoryTaskStore } from '../../src/experimental/tasks/stores/in-memory.js';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from './middleware.js';
import { OAuthClientProvider } from './auth.js';
import { FetchLike } from '../shared/transport.js';
import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from '../../src/client/middleware.js';
import { OAuthClientProvider } from '../../src/client/auth.js';
import { FetchLike } from '../../src/shared/transport.js';
import { MockInstance, Mocked, MockedFunction } from 'vitest';

vi.mock('../client/auth.js', async () => {
const actual = await vi.importActual<typeof import('../client/auth.js')>('../client/auth.js');
vi.mock('../../src/client/auth.js', async () => {
const actual = await vi.importActual<typeof import('../../src/client/auth.js')>('../../src/client/auth.js');
return {
...actual,
auth: vi.fn(),
extractWWWAuthenticateParams: vi.fn()
};
});

import { auth, extractWWWAuthenticateParams } from './auth.js';
import { auth, extractWWWAuthenticateParams } from '../../src/client/auth.js';

const mockAuth = auth as MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as MockedFunction<typeof extractWWWAuthenticateParams>;
Expand Down
Loading
Loading