mcp-oauth-provider.test.mjs
184 lines 6.1 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Legacy MCP OAuth provider suite — updated for Phase A durable refreshStore requirement.
3 */
4
5 import { describe, it, afterEach } from 'node:test';
6 import assert from 'node:assert/strict';
7 import {
8 createDurableMcpProvider,
9 mintMcpTokens,
10 } from './helpers/durable-mcp-oauth-harness.mjs';
11
12 const cleanups = [];
13 afterEach(async () => {
14 while (cleanups.length) await cleanups.pop()();
15 });
16
17 async function createProvider() {
18 const ctx = await createDurableMcpProvider();
19 cleanups.push(ctx.cleanup);
20 return ctx.provider;
21 }
22
23 describe('KnowtationOAuthProvider', () => {
24 describe('clientsStore', () => {
25 it('starts with no clients', async () => {
26 const provider = await createProvider();
27 assert.equal(provider.clientsStore.getClient('nonexistent'), undefined);
28 });
29
30 it('registers and retrieves a client', async () => {
31 const provider = await createProvider();
32 const registered = provider.clientsStore.registerClient({
33 redirect_uris: [new URL('http://localhost:8080/callback')],
34 client_name: 'Test Client',
35 });
36 assert.ok(registered.client_id);
37 assert.ok(registered.client_id_issued_at);
38 const retrieved = provider.clientsStore.getClient(registered.client_id);
39 assert.equal(retrieved.client_id, registered.client_id);
40 assert.equal(retrieved.client_name, 'Test Client');
41 });
42
43 it('evicts oldest client when limit reached', async () => {
44 const provider = await createProvider();
45 const ids = [];
46 for (let i = 0; i < 502; i++) {
47 const c = provider.clientsStore.registerClient({
48 redirect_uris: [new URL(`http://localhost:${8000 + i}/cb`)],
49 client_name: `client-${i}`,
50 });
51 ids.push(c.client_id);
52 }
53 assert.equal(provider.clientsStore.getClient(ids[0]), undefined);
54 assert.ok(provider.clientsStore.getClient(ids[ids.length - 1]));
55 });
56 });
57
58 describe('authorize', () => {
59 it('redirects to login page with mcp_state', async () => {
60 const provider = await createProvider();
61 const client = provider.clientsStore.registerClient({
62 redirect_uris: [new URL('http://localhost:8080/callback')],
63 });
64
65 let redirectUrl = null;
66 await provider.authorize(
67 client,
68 {
69 codeChallenge: 'test-challenge',
70 redirectUri: 'http://localhost:8080/callback',
71 state: 'client-state-123',
72 scopes: ['vault:read'],
73 },
74 { redirect(url) { redirectUrl = url; } }
75 );
76
77 assert.ok(redirectUrl);
78 assert.ok(redirectUrl.includes('/auth/login'));
79 assert.ok(redirectUrl.includes('mcp_state='));
80 });
81 });
82
83 describe('full authorization code flow', () => {
84 it('exchanges code for tokens after authorization completes', async () => {
85 const provider = await createProvider();
86 const { tokens } = await mintMcpTokens(provider, {
87 scopes: ['vault:read', 'vault:write'],
88 userId: 'google:12345',
89 });
90 assert.ok(tokens.access_token);
91 assert.equal(tokens.token_type, 'bearer');
92 assert.ok(tokens.expires_in > 0);
93 assert.ok(tokens.refresh_token);
94 assert.ok(tokens.scope.includes('vault:read'));
95 });
96 });
97
98 describe('challengeForAuthorizationCode', () => {
99 it('returns the stored code challenge', async () => {
100 const provider = await createProvider();
101 const client = provider.clientsStore.registerClient({
102 redirect_uris: [new URL('http://localhost:8080/callback')],
103 });
104
105 let redirectUrl = null;
106 await provider.authorize(
107 client,
108 {
109 codeChallenge: 'my-challenge-value',
110 redirectUri: 'http://localhost:8080/callback',
111 },
112 { redirect(url) { redirectUrl = url; } }
113 );
114
115 const url = new URL(redirectUrl);
116 const mcpState = url.searchParams.get('mcp_state');
117 const decoded = JSON.parse(Buffer.from(mcpState, 'base64url').toString());
118 const code = decoded.code;
119
120 const challenge = await provider.challengeForAuthorizationCode(client, code);
121 assert.equal(challenge, 'my-challenge-value');
122 });
123 });
124
125 describe('verifyAccessToken', () => {
126 it('verifies a valid MCP access token', async () => {
127 const provider = await createProvider();
128 const { client, tokens } = await mintMcpTokens(provider, {
129 scopes: ['vault:read'],
130 userId: 'github:99',
131 });
132 const authInfo = await provider.verifyAccessToken(tokens.access_token);
133 assert.equal(authInfo.clientId, client.client_id);
134 assert.ok(authInfo.scopes.includes('vault:read'));
135 assert.ok(authInfo.expiresAt);
136 assert.equal(authInfo.extra.sub, 'github:99');
137 });
138
139 it('rejects invalid token', async () => {
140 const provider = await createProvider();
141 await assert.rejects(
142 () => provider.verifyAccessToken('invalid-token'),
143 /Invalid access token/
144 );
145 });
146 });
147
148 describe('exchangeRefreshToken', () => {
149 it('issues new tokens from refresh token', async () => {
150 const provider = await createProvider();
151 const { client, tokens } = await mintMcpTokens(provider, {
152 scopes: ['vault:read'],
153 userId: 'google:1',
154 });
155 const next = await provider.exchangeRefreshToken(client, tokens.refresh_token);
156 assert.ok(next.access_token);
157 assert.ok(next.refresh_token);
158 });
159 });
160
161 describe('revokeToken', () => {
162 it('revokes a refresh token', async () => {
163 const provider = await createProvider();
164 const { client, tokens } = await mintMcpTokens(provider, { userId: 'google:1' });
165 await provider.revokeToken(client, { token: tokens.refresh_token });
166 await assert.rejects(
167 () => provider.exchangeRefreshToken(client, tokens.refresh_token),
168 /Unknown refresh token|revoked/i
169 );
170 });
171 });
172
173 describe('completeMcpAuthorization', () => {
174 it('rejects invalid mcp_state', async () => {
175 const provider = await createProvider();
176 let statusCode = null;
177 provider.completeMcpAuthorization('not-valid-base64!', 'user:1', {
178 status(code) { statusCode = code; return { json() {} }; },
179 redirect() {},
180 });
181 assert.equal(statusCode, 400);
182 });
183 });
184 });
File History 4 commits
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b fix(7C-L1b): route hosted delegation proposals through cani… Human minor 30 days ago
sha256:2827ba9e7632a4b141c50caf1e8f7d77abbc3515be20e7465f2bccb0ac4edf91 fix: repair endpoint now sets has_active_subscription when … Human minor 50 days ago