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

Viewing files at commit a93dad91899fCommit detailsView latest

api/auth0.ts 76 lines · 2.8 KB · typescript
1
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose';
2
3
export const defaultAuth0Domain = 'icfg-0ua6bab8d4omtfolx72mrhzo.us.auth0.com';
4
export const defaultAuth0ClientId = 'vGxzZd4LiO7TqH4U61QblwH96YcimpcA';
5
export const defaultAudience = 'https://openclasp.vercel.app/mcp';
6
7
const jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
8
9
export function auth0Config() {
10
  const domain = (
11
    process.env.AUTH0_DOMAIN ??
12
    process.env.NEXT_PUBLIC_AUTH0_DOMAIN ??
13
    defaultAuth0Domain
14
  )
15
    .replace(/^https?:\/\//, '')
16
    .replace(/\/$/, '');
17
  return {
18
    domain,
19
    issuer: `https://${domain}/`,
20
    audience: process.env.AUTH0_AUDIENCE ?? process.env.OPENCLASP_MCP_URL ?? defaultAudience,
21
    dashboardClientId:
22
      process.env.AUTH0_CLIENT_ID ??
23
      process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID ??
24
      defaultAuth0ClientId,
25
  };
26
}
27
28
function scopes(payload: JWTPayload): string[] {
29
  const scope = typeof payload.scope === 'string' ? payload.scope.split(/\s+/) : [];
30
  const permissions = Array.isArray(payload.permissions)
31
    ? payload.permissions.filter((value): value is string => typeof value === 'string')
32
    : [];
33
  return [...new Set([...scope, ...permissions])];
34
}
35
36
export async function verifyAuth0Token(
37
  token: string,
38
  options: { dashboard?: boolean; requiredScopes?: string[] } = {},
39
) {
40
  const config = auth0Config();
41
  let remote = jwks.get(config.issuer);
42
  if (!remote) {
43
    remote = createRemoteJWKSet(new URL(`${config.issuer}.well-known/jwks.json`));
44
    jwks.set(config.issuer, remote);
45
  }
46
  const { payload } = await jwtVerify(token, remote, {
47
    issuer: config.issuer,
48
    audience: config.audience,
49
    algorithms: ['RS256'],
50
  });
51
  if (!payload.sub) throw new Error('Access token has no subject');
52
  const clientId =
53
    typeof payload.client_id === 'string'
54
      ? payload.client_id
55
      : typeof payload.azp === 'string'
56
        ? payload.azp
57
        : undefined;
58
  if (!clientId) throw new Error('Access token has no client identifier');
59
  if (options.dashboard === true && clientId !== config.dashboardClientId)
60
    throw new Error('Agent token cannot access the dashboard API');
61
  if (options.dashboard === false && clientId === config.dashboardClientId)
62
    throw new Error('Dashboard token cannot access MCP');
63
  const grantedScopes = scopes(payload);
64
  if (options.requiredScopes?.some((scope) => !grantedScopes.includes(scope)))
65
    throw new Error('Access token is missing a required scope');
66
  return { payload, clientId, scopes: grantedScopes };
67
}
68
69
export async function loadAuth0Profile(token: string) {
70
  const { domain } = auth0Config();
71
  const response = await fetch(`https://${domain}/userinfo`, {
72
    headers: { authorization: `Bearer ${token}` },
73
  });
74
  if (!response.ok) return {};
75
  return (await response.json()) as { email?: string; name?: string };
76
}