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

Viewing files at commit 2c749578ec22Commit detailsView latest

api/oauth-token.ts 96 lines · 3.3 KB · typescript
1
import { createHash, timingSafeEqual } from 'node:crypto';
2
import { oauthStore, type OAuthStore } from './oauth-store.js';
3
4
const noStoreHeaders = {
5
  'access-control-allow-origin': '*',
6
  'cache-control': 'no-store',
7
  pragma: 'no-cache',
8
};
9
10
function tokenError(error: string, description: string, status = 400): Response {
11
  return Response.json(
12
    { error, error_description: description },
13
    { status, headers: noStoreHeaders },
14
  );
15
}
16
17
function matchesChallenge(verifier: string, expected: string): boolean {
18
  const actual = createHash('sha256').update(verifier).digest('base64url');
19
  const left = Buffer.from(actual);
20
  const right = Buffer.from(expected);
21
  return left.length === right.length && timingSafeEqual(left, right);
22
}
23
24
export async function exchange(request: Request, store: OAuthStore): Promise<Response> {
25
  const form = new URLSearchParams(await request.text());
26
  const grantType = form.get('grant_type');
27
  const clientId = form.get('client_id');
28
  if (!clientId) return tokenError('invalid_client', 'client_id is required', 401);
29
30
  if (grantType === 'authorization_code') {
31
    const code = form.get('code');
32
    const redirectUri = form.get('redirect_uri');
33
    const verifier = form.get('code_verifier');
34
    if (!code || !redirectUri || !verifier)
35
      return tokenError('invalid_request', 'Code, redirect URI, and PKCE verifier are required');
36
    const authorization = await store.takeAuthorizationCode(code);
37
    if (
38
      !authorization ||
39
      authorization.clientId !== clientId ||
40
      authorization.redirectUri !== redirectUri ||
41
      !matchesChallenge(verifier, authorization.codeChallenge)
42
    )
43
      return tokenError('invalid_grant', 'Authorization code is invalid or expired');
44
    const scopes = authorization.scope.split(/\s+/).filter(Boolean);
45
    const issued = await store.issueTokens(clientId, authorization.operatorId, scopes);
46
    return Response.json(
47
      {
48
        access_token: issued.accessToken,
49
        token_type: 'Bearer',
50
        expires_in: issued.expiresIn,
51
        refresh_token: issued.refreshToken,
52
        scope: scopes.join(' '),
53
      },
54
      { headers: noStoreHeaders },
55
    );
56
  }
57
58
  if (grantType === 'refresh_token') {
59
    const refreshToken = form.get('refresh_token');
60
    if (!refreshToken) return tokenError('invalid_request', 'refresh_token is required');
61
    const issued = await store.rotateRefreshToken(refreshToken, clientId);
62
    if (!issued) return tokenError('invalid_grant', 'Refresh token is invalid or expired');
63
    return Response.json(
64
      {
65
        access_token: issued.accessToken,
66
        token_type: 'Bearer',
67
        expires_in: issued.expiresIn,
68
        refresh_token: issued.refreshToken,
69
        scope: issued.scopes.join(' '),
70
      },
71
      { headers: noStoreHeaders },
72
    );
73
  }
74
75
  return tokenError('unsupported_grant_type', 'Unsupported grant type');
76
}
77
78
export async function POST(request: Request): Promise<Response> {
79
  try {
80
    return await exchange(request, oauthStore());
81
  } catch {
82
    return tokenError('temporarily_unavailable', 'OAuth service unavailable', 503);
83
  }
84
}
85
86
export function OPTIONS(): Response {
87
  return new Response(null, {
88
    status: 204,
89
    headers: {
90
      'access-control-allow-headers': 'content-type',
91
      'access-control-allow-methods': 'POST, OPTIONS',
92
      'access-control-allow-origin': '*',
93
      'access-control-max-age': '86400',
94
    },
95
  });
96
}