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

Viewing files at commit 160755529234Commit detailsView latest

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