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

Viewing files at commit e9e4992bde95Commit detailsView latest

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