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

Viewing files at commit a9e4757bb766Commit detailsView latest

api/oauth-authorize.ts 67 lines · 2.7 KB · typescript
1
import { createHash } from 'node:crypto';
2
import { auth0Config } from './auth0.js';
3
import { oauthStore, randomOAuthValue, type OAuthStore } from './oauth-store.js';
4
5
const allowedScopes = new Set(['mcp:access']);
6
7
function oauthError(error: string, description: string, status = 400): Response {
8
  return Response.json({ error, error_description: description }, { status });
9
}
10
11
export async function authorize(request: Request, store: OAuthStore): Promise<Response> {
12
  const url = new URL(request.url);
13
  const clientId = url.searchParams.get('client_id');
14
  const redirectUri = url.searchParams.get('redirect_uri');
15
  const responseType = url.searchParams.get('response_type');
16
  const codeChallenge = url.searchParams.get('code_challenge');
17
  const challengeMethod = url.searchParams.get('code_challenge_method');
18
  if (!clientId || !redirectUri)
19
    return oauthError('invalid_request', 'Missing client or redirect URI');
20
  const client = await store.getClient(clientId);
21
  if (!client || !client.redirectUris.includes(redirectUri))
22
    return oauthError('invalid_request', 'Unknown client or redirect URI');
23
  if (responseType !== 'code') return oauthError('unsupported_response_type', 'Use code');
24
  if (!codeChallenge || challengeMethod !== 'S256')
25
    return oauthError('invalid_request', 'PKCE with S256 is required');
26
27
  const requestedScopes = (url.searchParams.get('scope') ?? 'mcp:access')
28
    .split(/\s+/)
29
    .filter(Boolean);
30
  if (!requestedScopes.length || requestedScopes.some((scope) => !allowedScopes.has(scope)))
31
    return oauthError('invalid_scope', 'Only mcp:access is supported');
32
33
  const transactionId = randomOAuthValue('oc_tx_');
34
  const auth0Verifier = randomOAuthValue('');
35
  const auth0Challenge = createHash('sha256').update(auth0Verifier).digest('base64url');
36
  await store.createTransaction({
37
    transactionId,
38
    clientId,
39
    redirectUri,
40
    ...(url.searchParams.has('state') ? { downstreamState: url.searchParams.get('state')! } : {}),
41
    scope: requestedScopes.join(' '),
42
    codeChallenge,
43
    auth0Verifier,
44
  });
45
46
  const config = auth0Config();
47
  const auth0Url = new URL(`${config.issuer}authorize`);
48
  auth0Url.search = new URLSearchParams({
49
    client_id: config.dashboardClientId,
50
    redirect_uri: `${config.publicOrigin}/sso-callback`,
51
    response_type: 'code',
52
    scope: 'openid profile email',
53
    audience: config.audience,
54
    state: transactionId,
55
    code_challenge: auth0Challenge,
56
    code_challenge_method: 'S256',
57
  }).toString();
58
  return Response.redirect(auth0Url, 302);
59
}
60
61
export async function GET(request: Request): Promise<Response> {
62
  try {
63
    return await authorize(request, oauthStore());
64
  } catch {
65
    return oauthError('temporarily_unavailable', 'OAuth service unavailable', 503);
66
  }
67
}