maestro/src/bridge/a2a/token-auth.ts
oss-sync b1292e34b2
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (edc775f2)
2026-07-06 01:04:12 +00:00

68 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type Provider from 'oidc-provider';
import type { Repository } from '../../db/repository.js';
import { isDelegationLive, type A2aDelegation } from './delegation.js';
import { logger } from '../../logger.js';
import { A2A_READ_SCOPE } from './oidc-config.js';
export interface A2aPrincipal {
actingUserId: string;
clientId: string;
grantId: string;
delegation: A2aDelegation;
}
function extractBearer(headers: Record<string, unknown>): string | null {
const raw = headers['authorization'] ?? headers['Authorization'];
const v = Array.isArray(raw) ? raw[0] : raw;
if (typeof v !== 'string') return null;
const m = /^Bearer\s+(.+)$/i.exec(v.trim());
return m ? m[1] : null;
}
/**
* inbound A2A リクエストの認証。fail-closed: いずれか失敗で null。
* 1) Bearer 抽出 2) provider.AccessToken.find期限切れは undefined
* 3) accountId/grantId 必須 4) a2a.read スコープ必須 5) アカウント active 必須
* 6) grant_id → 委任 7) 委任が live。
*/
export async function authenticateA2aRequest(
provider: Provider,
repo: Repository,
req: { headers: Record<string, unknown> },
nowIso: string,
): Promise<A2aPrincipal | null> {
const token = extractBearer(req.headers);
if (!token) return null;
let at: { accountId?: string; grantId?: string; scope?: string } | undefined;
try {
at = await provider.AccessToken.find(token);
} catch (err) {
logger.warn(`[a2a] token find failed: ${err}`);
return null;
}
if (!at || !at.accountId || !at.grantId) return null;
// M1: a2a.read スコープが無いトークンは非 A2Afail-closed
const scopes = String(at.scope ?? '').split(' ').filter(Boolean);
if (!scopes.includes(A2A_READ_SCOPE)) return null;
// I1: アカウントが active でなければ拒否(無効アカウント・存在しないアカウントも含む)
const acct = repo.getUserById(at.accountId);
if (!acct || acct.status !== 'active') return null;
const row = repo.getA2aDelegationByGrantId(at.grantId);
if (!row) return null;
if (!isDelegationLive(row, nowIso)) return null;
// acting user は委任行の user_id と一致するはず(トークンの accountId と二重確認)
if (row.userId !== at.accountId) { logger.warn('[a2a] token/delegation user mismatch'); return null; }
const delegation: A2aDelegation = {
id: row.id,
userId: row.userId, clientId: row.clientId, grantId: at.grantId,
grantedSpaceIds: row.grantedSpaceIds, grantedSkills: row.grantedSkills,
expiresAt: row.expiresAt, revokedAt: row.revokedAt,
};
return { actingUserId: at.accountId, clientId: row.clientId, grantId: at.grantId, delegation };
}