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

Viewing files at commit 8974f6e2faf1Commit detailsView latest

apps/api/src/auth0.ts 99 lines · 3.5 KB · typescript
1
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose';
2
3
// Shared runtime code lives outside /api so Vercel does not count it as a function.
4
5
export const defaultAuth0Domain = 'icfg-0ua6bab8d4omtfolx72mrhzo.us.auth0.com';
6
export const defaultAuth0ClientId = 'vGxzZd4LiO7TqH4U61QblwH96YcimpcA';
7
export const defaultAudience = 'https://openclasp.vercel.app/mcp';
8
export const dashboardSessionCookie = 'openclasp_session';
9
10
const jwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
11
12
export function auth0Config() {
13
  const domain = (
14
    process.env.AUTH0_DOMAIN ??
15
    process.env.NEXT_PUBLIC_AUTH0_DOMAIN ??
16
    defaultAuth0Domain
17
  )
18
    .replace(/^https?:\/\//, '')
19
    .replace(/\/$/, '');
20
  const audience = process.env.AUTH0_AUDIENCE ?? process.env.OPENCLASP_MCP_URL ?? defaultAudience;
21
  return {
22
    domain,
23
    issuer: `https://${domain}/`,
24
    audience,
25
    publicOrigin: new URL(
26
      process.env.OPENCLASP_PUBLIC_URL ?? (URL.canParse(audience) ? audience : defaultAudience),
27
    ).origin,
28
    dashboardClientId:
29
      process.env.AUTH0_CLIENT_ID ??
30
      process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID ??
31
      defaultAuth0ClientId,
32
  };
33
}
34
35
function scopes(payload: JWTPayload): string[] {
36
  const scope = typeof payload.scope === 'string' ? payload.scope.split(/\s+/) : [];
37
  const permissions = Array.isArray(payload.permissions)
38
    ? payload.permissions.filter((value): value is string => typeof value === 'string')
39
    : [];
40
  return [...new Set([...scope, ...permissions])];
41
}
42
43
export async function verifyAuth0Token(
44
  token: string,
45
  options: { dashboard?: boolean; requiredScopes?: string[] } = {},
46
) {
47
  const config = auth0Config();
48
  let remote = jwks.get(config.issuer);
49
  if (!remote) {
50
    remote = createRemoteJWKSet(new URL(`${config.issuer}.well-known/jwks.json`));
51
    jwks.set(config.issuer, remote);
52
  }
53
  const { payload } = await jwtVerify(token, remote, {
54
    issuer: config.issuer,
55
    audience: config.audience,
56
    algorithms: ['RS256'],
57
  });
58
  if (!payload.sub) throw new Error('Access token has no subject');
59
  const clientId =
60
    typeof payload.client_id === 'string'
61
      ? payload.client_id
62
      : typeof payload.azp === 'string'
63
        ? payload.azp
64
        : undefined;
65
  if (!clientId) throw new Error('Access token has no client identifier');
66
  if (options.dashboard === true && clientId !== config.dashboardClientId)
67
    throw new Error('Agent token cannot access the dashboard API');
68
  if (options.dashboard === false && clientId === config.dashboardClientId)
69
    throw new Error('Dashboard token cannot access MCP');
70
  const grantedScopes = scopes(payload);
71
  if (options.requiredScopes?.some((scope) => !grantedScopes.includes(scope)))
72
    throw new Error('Access token is missing a required scope');
73
  return { payload, clientId, scopes: grantedScopes };
74
}
75
76
export async function loadAuth0Profile(token: string) {
77
  const { domain } = auth0Config();
78
  const response = await fetch(`https://${domain}/userinfo`, {
79
    headers: { authorization: `Bearer ${token}` },
80
  });
81
  if (!response.ok) return {};
82
  return (await response.json()) as {
83
    sub?: string;
84
    email?: string;
85
    name?: string;
86
    picture?: string;
87
  };
88
}
89
90
export function dashboardTokenFromCookie(header?: string): string | undefined {
91
  if (!header) return undefined;
92
  for (const entry of header.split(';')) {
93
    const separator = entry.indexOf('=');
94
    if (separator < 0) continue;
95
    if (entry.slice(0, separator).trim() === dashboardSessionCookie)
96
      return decodeURIComponent(entry.slice(separator + 1).trim());
97
  }
98
  return undefined;
99
}