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

@@ -8,6 +8,7 @@ import { CredentialRepository } from '../repositories/CredentialRepository.js';
import { AgentRepository } from '../repositories/AgentRepository.js';
import { AuditService } from './AuditService.js';
import { VaultClient } from '../vault/VaultClient.js';
import { IDTokenService } from './IDTokenService.js';
import {
ITokenPayload,
ITokenResponse,
@@ -46,6 +47,8 @@ export class OAuth2Service {
* @param privateKey - PEM-encoded RSA private key for signing tokens.
* @param publicKey - PEM-encoded RSA public key for verifying tokens.
* @param vaultClient - Optional VaultClient for Phase 2 credential verification.
* @param idTokenService - Optional IDTokenService; when provided and `openid` scope
* is requested, an OIDC ID token is appended to the token response.
*/
constructor(
private readonly tokenRepository: TokenRepository,
@@ -55,6 +58,7 @@ export class OAuth2Service {
private readonly privateKey: string,
private readonly publicKey: string,
private readonly vaultClient: VaultClient | null = null,
private readonly idTokenService: IDTokenService | null = null,
) {}
/**
@@ -207,12 +211,21 @@ export class OAuth2Service {
// Instrument: count successful token issuances
tokensIssuedTotal.inc({ scope });
return {
const tokenResponse: ITokenResponse = {
access_token: accessToken,
token_type: 'Bearer',
expires_in: expiresIn,
scope,
};
// OIDC: append id_token when the `openid` scope was requested and IDTokenService is wired
const scopeList = scope.split(' ');
if (scopeList.includes('openid') && this.idTokenService !== null) {
const claims = await this.idTokenService.buildIDTokenClaims(agent, clientId, scope);
tokenResponse.id_token = await this.idTokenService.signIDToken(claims);
}
return tokenResponse;
}
/**