The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 5dab768c7927Commit detailsView latest

tests/oauth-proxy.test.ts 206 lines · 7.7 KB · typescript
1
import { createHash } from 'node:crypto';
2
import { afterEach, describe, expect, it, vi } from 'vitest';
3
import { GET as authorizationMetadata } from '../api/oauth-authorization-server.js';
4
import { authorize } from '../api/oauth-authorize.js';
5
import { register } from '../api/oauth-register.js';
6
import type { OAuthStore, OAuthTransaction } from '../apps/api/src/oauth-store.js';
7
import { exchange } from '../api/oauth-token.js';
8
9
afterEach(() => vi.restoreAllMocks());
10
11
describe('OpenClasp OAuth broker', () => {
12
  it('publishes OpenClasp-owned OAuth endpoints', async () => {
13
    const response = await authorizationMetadata(
14
      new Request('https://openclasp.example/.well-known/oauth-authorization-server'),
15
    );
16
    await expect(response.json()).resolves.toMatchObject({
17
      issuer: 'https://openclasp.example',
18
      authorization_endpoint: 'https://openclasp.example/oauth/authorize',
19
      token_endpoint: 'https://openclasp.example/oauth/token',
20
      registration_endpoint: 'https://openclasp.example/oauth/register',
21
      scopes_supported: expect.arrayContaining([
22
        'mcp:access',
23
        'profile:read',
24
        'interaction:write',
25
        'feedback:write',
26
        'agent:manage',
27
      ]),
28
      code_challenge_methods_supported: ['S256'],
29
      token_endpoint_auth_methods_supported: ['none'],
30
    });
31
  });
32
33
  it('registers public clients in OpenClasp instead of Auth0', async () => {
34
    const registerClient = vi.fn().mockResolvedValue({
35
      clientId: 'oc_client',
36
      redirectUris: ['http://localhost:8787/callback'],
37
    });
38
    const input = {
39
      client_name: 'MCP client',
40
      redirect_uris: ['http://localhost:8787/callback'],
41
      token_endpoint_auth_method: 'none',
42
    };
43
    vi.spyOn(console, 'info').mockImplementation(() => undefined);
44
    const response = await register(
45
      new Request('https://openclasp.example/oauth/register', {
46
        method: 'POST',
47
        body: JSON.stringify(input),
48
      }),
49
      { registerClient } as unknown as OAuthStore,
50
    );
51
    expect(response.status).toBe(201);
52
    expect(registerClient).toHaveBeenCalledOnce();
53
    await expect(response.json()).resolves.toMatchObject({
54
      client_id: 'oc_client',
55
      token_endpoint_auth_method: 'none',
56
      grant_types: ['authorization_code', 'refresh_token'],
57
    });
58
  });
59
60
  it('rejects unsafe web redirect URIs', async () => {
61
    const registerClient = vi.fn();
62
    const response = await register(
63
      new Request('https://openclasp.example/oauth/register', {
64
        method: 'POST',
65
        body: JSON.stringify({ redirect_uris: ['http://attacker.example/callback'] }),
66
      }),
67
      { registerClient } as unknown as OAuthStore,
68
    );
69
    expect(response.status).toBe(400);
70
    expect(registerClient).not.toHaveBeenCalled();
71
  });
72
73
  it('brokers authorization through the existing Auth0 application', async () => {
74
    const createTransaction = vi.fn().mockResolvedValue(undefined);
75
    const store = {
76
      getClient: vi.fn().mockResolvedValue({
77
        clientId: 'oc_client',
78
        redirectUris: ['http://127.0.0.1:4567/callback'],
79
      }),
80
      createTransaction,
81
    } as unknown as OAuthStore;
82
    const challenge = createHash('sha256').update('downstream-verifier').digest('base64url');
83
    const request = new Request(
84
      `https://openclasp.example/oauth/authorize?${new URLSearchParams({
85
        client_id: 'oc_client',
86
        redirect_uri: 'http://127.0.0.1:4567/callback',
87
        response_type: 'code',
88
        scope: 'mcp:access profile:read interaction:write',
89
        state: 'client-state',
90
        code_challenge: challenge,
91
        code_challenge_method: 'S256',
92
      })}`,
93
    );
94
    const response = await authorize(request, store);
95
    expect(response.status).toBe(302);
96
    const destination = new URL(response.headers.get('location')!);
97
    expect(destination.origin).toBe('https://icfg-0ua6bab8d4omtfolx72mrhzo.us.auth0.com');
98
    expect(destination.searchParams.get('client_id')).toBe('vGxzZd4LiO7TqH4U61QblwH96YcimpcA');
99
    expect(destination.searchParams.get('redirect_uri')).toBe(
100
      'https://openclasp.vercel.app/sso-callback',
101
    );
102
    const saved = createTransaction.mock.calls[0]?.[0] as OAuthTransaction;
103
    expect(saved.clientId).toBe('oc_client');
104
    expect(saved.downstreamState).toBe('client-state');
105
    expect(saved.codeChallenge).toBe(challenge);
106
    expect(saved.scope).toBe('mcp:access profile:read interaction:write');
107
  });
108
109
  it('does not let integration credentials opt an account into network contribution', async () => {
110
    const store = {
111
      getClient: vi.fn().mockResolvedValue({
112
        clientId: 'oc_client',
113
        redirectUris: ['http://127.0.0.1:4567/callback'],
114
      }),
115
      createTransaction: vi.fn(),
116
    } as unknown as OAuthStore;
117
    const response = await authorize(
118
      new Request(
119
        `https://openclasp.example/oauth/authorize?${new URLSearchParams({
120
          client_id: 'oc_client',
121
          redirect_uri: 'http://127.0.0.1:4567/callback',
122
          response_type: 'code',
123
          scope: 'mcp:access network:contribute',
124
          code_challenge: 'challenge',
125
          code_challenge_method: 'S256',
126
        })}`,
127
      ),
128
      store,
129
    );
130
    expect(response.status).toBe(400);
131
    await expect(response.json()).resolves.toMatchObject({ error: 'invalid_scope' });
132
    expect(store.createTransaction).not.toHaveBeenCalled();
133
  });
134
135
  it('exchanges a PKCE code for OpenClasp opaque tokens', async () => {
136
    const verifier = 'a-secure-downstream-verifier';
137
    const codeChallenge = createHash('sha256').update(verifier).digest('base64url');
138
    const issueTokens = vi.fn().mockResolvedValue({
139
      accessToken: 'oc_oat_access',
140
      refreshToken: 'oc_rt_refresh',
141
      expiresIn: 3600,
142
    });
143
    const store = {
144
      takeAuthorizationCode: vi.fn().mockResolvedValue({
145
        clientId: 'oc_client',
146
        operatorId: 'auth0|user',
147
        redirectUri: 'http://localhost:8787/callback',
148
        scope: 'mcp:access',
149
        codeChallenge,
150
      }),
151
      issueTokens,
152
    } as unknown as OAuthStore;
153
    const response = await exchange(
154
      new Request('https://openclasp.example/oauth/token', {
155
        method: 'POST',
156
        body: new URLSearchParams({
157
          grant_type: 'authorization_code',
158
          client_id: 'oc_client',
159
          code: 'oc_code_value',
160
          redirect_uri: 'http://localhost:8787/callback',
161
          code_verifier: verifier,
162
        }),
163
      }),
164
      store,
165
    );
166
    expect(response.status).toBe(200);
167
    await expect(response.json()).resolves.toEqual({
168
      access_token: 'oc_oat_access',
169
      token_type: 'Bearer',
170
      expires_in: 3600,
171
      refresh_token: 'oc_rt_refresh',
172
      scope: 'mcp:access',
173
    });
174
    expect(issueTokens).toHaveBeenCalledWith('oc_client', 'auth0|user', ['mcp:access']);
175
  });
176
177
  it('rejects an incorrect PKCE verifier', async () => {
178
    const issueTokens = vi.fn();
179
    const store = {
180
      takeAuthorizationCode: vi.fn().mockResolvedValue({
181
        clientId: 'oc_client',
182
        operatorId: 'auth0|user',
183
        redirectUri: 'http://localhost:8787/callback',
184
        scope: 'mcp:access',
185
        codeChallenge: createHash('sha256').update('correct-verifier').digest('base64url'),
186
      }),
187
      issueTokens,
188
    } as unknown as OAuthStore;
189
    const response = await exchange(
190
      new Request('https://openclasp.example/oauth/token', {
191
        method: 'POST',
192
        body: new URLSearchParams({
193
          grant_type: 'authorization_code',
194
          client_id: 'oc_client',
195
          code: 'oc_code_value',
196
          redirect_uri: 'http://localhost:8787/callback',
197
          code_verifier: 'wrong-verifier',
198
        }),
199
      }),
200
      store,
201
    );
202
    expect(response.status).toBe(400);
203
    await expect(response.json()).resolves.toMatchObject({ error: 'invalid_grant' });
204
    expect(issueTokens).not.toHaveBeenCalled();
205
  });
206
});