68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
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 スコープが無いトークンは非 A2A(fail-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 };
|
||
}
|