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

Viewing files at commit 92498f6892e8Commit detailsView latest

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