feat(phase-3): workstream 3 — OpenID Connect (OIDC) Provider

Implements full OIDC layer on top of the existing OAuth 2.0 token service:

- Migration 014: oidc_keys table (RSA/EC key pairs, is_current flag, expires_at
  for rotation grace period)
- OIDCKeyService: key generation (RS256/ES256), Vault storage, JWKS with Redis
  cache, key rotation with grace period, pruneExpiredKeys
- IDTokenService: buildIDTokenClaims (agent claims, nonce, DID), signIDToken
  (kid in JWT header), verifyIDToken (alg:none rejected, RS256/ES256 only)
- OIDCController: discovery document, JWKS (Cache-Control), /agent-info
- OIDC routes mounted at / — /.well-known/openid-configuration,
  /.well-known/jwks.json, /agent-info
- OAuth2Service: id_token appended to token response when openid scope requested
- 473 unit tests passing (100% OIDCKeyService stmts, 95.91% IDTokenService stmts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-30 09:54:26 +00:00
parent 3d1fff15f6
commit 5e465e596a
13 changed files with 2221 additions and 13 deletions

View File

@@ -24,6 +24,8 @@ import { CredentialService } from './services/CredentialService.js';
import { OAuth2Service } from './services/OAuth2Service.js';
import { OrgService } from './services/OrgService.js';
import { DIDService } from './services/DIDService.js';
import { OIDCKeyService } from './services/OIDCKeyService.js';
import { IDTokenService } from './services/IDTokenService.js';
import { AgentController } from './controllers/AgentController.js';
import { TokenController } from './controllers/TokenController.js';
@@ -31,6 +33,7 @@ import { CredentialController } from './controllers/CredentialController.js';
import { AuditController } from './controllers/AuditController.js';
import { OrgController } from './controllers/OrgController.js';
import { DIDController } from './controllers/DIDController.js';
import { OIDCController } from './controllers/OIDCController.js';
import { createAgentsRouter } from './routes/agents.js';
import { createTokenRouter } from './routes/token.js';
@@ -40,6 +43,7 @@ import { createHealthRouter } from './routes/health.js';
import { createMetricsRouter } from './routes/metrics.js';
import { createOrgsRouter } from './routes/organizations.js';
import { createDIDRouter } from './routes/did.js';
import { createOIDCRouter } from './routes/oidc.js';
import { errorHandler } from './middleware/errorHandler.js';
import { createOpaMiddleware } from './middleware/opa.js';
@@ -132,6 +136,11 @@ export async function createApp(): Promise<Application> {
throw new Error('JWT_PRIVATE_KEY and JWT_PUBLIC_KEY environment variables are required');
}
// OIDC services — initialised after DB pool is ready
const oidcKeyService = new OIDCKeyService(pool, redis as RedisClientType);
await oidcKeyService.ensureCurrentKey();
const idTokenService = new IDTokenService(oidcKeyService);
const oauth2Service = new OAuth2Service(
tokenRepo,
credentialRepo,
@@ -140,6 +149,7 @@ export async function createApp(): Promise<Application> {
privateKey,
publicKey,
vaultClient,
idTokenService,
);
// ────────────────────────────────────────────────────────────────
@@ -156,6 +166,7 @@ export async function createApp(): Promise<Application> {
const auditController = new AuditController(auditService);
const orgController = new OrgController(orgService);
const didController = new DIDController(didService, agentRepo);
const oidcController = new OIDCController(oidcKeyService, agentRepo);
// ────────────────────────────────────────────────────────────────
// Org context middleware — sets PostgreSQL session variable app.organization_id
@@ -180,6 +191,9 @@ export async function createApp(): Promise<Application> {
void didController.getInstanceDIDDocument(req, res, next);
});
// OIDC well-known endpoints and agent-info — mounted at root so /.well-known/* paths resolve
app.use('/', createOIDCRouter(oidcController, authMiddleware));
app.use(`${API_BASE}/agents`, createAgentsRouter(agentController, opaMiddleware));
app.use(`${API_BASE}`, createDIDRouter(didController, authMiddleware, opaMiddleware));
app.use(