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

Viewing files at commit d596f9595c49Commit detailsView latest

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