Archived 4 completed OpenSpec changes (2026-04-02): - phase-3-enterprise (100/100 tasks) — 6 Phase 3 capabilities synced - devops-documentation (48/48 tasks) — 3 new + 1 merged capability - bedroom-developer-docs (33/33 tasks) — 4 new capabilities synced - engineering-docs (superseded by 2026-03-29 archive) — no tasks Main spec library grows from 21 → 35 capabilities (+14 new): federation, multi-tenancy, oidc, soc2, w3c-dids, webhooks, database, operations, system-overview, api-reference, core-concepts, developer-guides, quick-start + deployment (merged additive requirements) Active changes: 0 — project board is clear for Phase 4 planning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
OpenID Connect (OIDC) — Specification
Workstream: 3 of 6 Phase: 3 — Enterprise Author: Virtual Architect Date: 2026-03-29
Overview
Add a full OIDC 1.0 layer on top of the existing OAuth 2.0 client_credentials implementation using the certified oidc-provider npm library. The OIDC layer exposes Discovery, JWKS, extends the token endpoint to return ID tokens with agent claims, and provides an /agent-info endpoint (the agent-identity equivalent of OIDC's /userinfo).
The existing POST /oauth2/token endpoint is extended, not replaced. Callers that do not request the openid scope continue to receive standard OAuth 2.0 responses unchanged.
API Endpoints
GET /.well-known/openid-configuration
OIDC Discovery document. No authentication required. This is the standard OIDC Discovery endpoint (RFC 8414 / OpenID Connect Discovery 1.0).
GET /.well-known/openid-configuration
No authentication required
Responses:
200 OK:
Content-Type: application/json
schema:
type: object
description: OIDC Discovery document per OpenID Connect Discovery 1.0
example:
issuer: "https://idp.sentryagent.ai"
authorization_endpoint: "https://idp.sentryagent.ai/oauth2/authorize"
token_endpoint: "https://idp.sentryagent.ai/oauth2/token"
jwks_uri: "https://idp.sentryagent.ai/.well-known/jwks.json"
userinfo_endpoint: "https://idp.sentryagent.ai/agent-info"
introspection_endpoint: "https://idp.sentryagent.ai/oauth2/introspect"
revocation_endpoint: "https://idp.sentryagent.ai/oauth2/revoke"
response_types_supported:
- "token"
grant_types_supported:
- "client_credentials"
subject_types_supported:
- "public"
id_token_signing_alg_values_supported:
- "RS256"
- "ES256"
scopes_supported:
- "openid"
- "agents:read"
- "agents:write"
- "tokens:read"
- "audit:read"
claims_supported:
- "sub"
- "iss"
- "iat"
- "exp"
- "agent_id"
- "agent_type"
- "organization_id"
- "capabilities"
- "deployment_env"
- "owner"
token_endpoint_auth_methods_supported:
- "client_secret_post"
- "client_secret_basic"
500 Internal Server Error:
schema:
$ref: '#/components/schemas/ErrorResponse'
GET /.well-known/jwks.json
JSON Web Key Set. Contains the public keys used to sign ID tokens and access tokens. No authentication required. Clients use this endpoint to verify token signatures.
GET /.well-known/jwks.json
No authentication required
Responses:
200 OK:
Content-Type: application/json
Cache-Control: public, max-age=3600
schema:
type: object
required: [keys]
properties:
keys:
type: array
items:
type: object
description: JSON Web Key (RFC 7517)
properties:
kty:
type: string
example: "RSA"
use:
type: string
example: "sig"
kid:
type: string
description: Key ID — matches `kid` header in issued JWTs
alg:
type: string
example: "RS256"
n:
type: string
description: RSA modulus (base64url)
e:
type: string
description: RSA exponent (base64url)
example:
keys:
- kty: "RSA"
use: "sig"
kid: "key-2026-03-29-01"
alg: "RS256"
n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt..."
e: "AQAB"
500 Internal Server Error:
schema:
$ref: '#/components/schemas/ErrorResponse'
POST /oauth2/token (extended)
The existing token endpoint is extended to return an id_token when the openid scope is requested. All existing behavior is preserved when openid is not in the scope list.
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Request Body:
schema:
type: object
required: [grant_type, client_id, client_secret]
properties:
grant_type:
type: string
enum: [client_credentials]
client_id:
type: string
client_secret:
type: string
scope:
type: string
description: Space-separated scopes. Include "openid" to receive an id_token.
example: "openid agents:read"
Responses:
200 OK (with openid scope):
schema:
type: object
properties:
access_token:
type: string
token_type:
type: string
example: "Bearer"
expires_in:
type: integer
scope:
type: string
id_token:
type: string
description: Signed JWT ID token containing agent identity claims. Only present when openid scope was requested.
example:
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
token_type: "Bearer"
expires_in: 3600
scope: "openid agents:read"
id_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0yMDI2LTAzLTI5LTAxIn0..."
200 OK (without openid scope):
schema:
type: object
properties:
access_token:
type: string
token_type:
type: string
expires_in:
type: integer
scope:
type: string
example:
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
token_type: "Bearer"
expires_in: 3600
scope: "agents:read"
400 Bad Request:
schema:
$ref: '#/components/schemas/OAuthErrorResponse'
example:
error: "invalid_client"
error_description: "Invalid client credentials"
401 Unauthorized:
schema:
$ref: '#/components/schemas/OAuthErrorResponse'
ID Token Claims
When openid scope is requested, the ID token (a signed JWT) contains the following claims:
{
"iss": "https://idp.sentryagent.ai",
"sub": "agt_01HXK7Z9P3FKWABCDEF67890",
"aud": "agt_01HXK7Z9P3FKWABCDEF67890",
"iat": 1743249600,
"exp": 1743253200,
"agent_id": "agt_01HXK7Z9P3FKWABCDEF67890",
"agent_type": "orchestrator",
"organization_id": "org_01HXK7Z9P3FKWABCDEF12345",
"capabilities": ["task-planning", "tool-use"],
"deployment_env": "production",
"owner": "acme-ai",
"did": "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
}
GET /agent-info
Returns claims about the authenticated agent identity. This is the agent-first equivalent of the OIDC /userinfo endpoint. Authentication required with any valid access token.
GET /agent-info
Authorization: Bearer <access_token>
Responses:
200 OK:
Content-Type: application/json
schema:
type: object
description: Agent identity claims (subset of registered agent data)
properties:
sub:
type: string
description: Subject — agentId
agent_id:
type: string
agent_type:
type: string
organization_id:
type: string
capabilities:
type: array
items:
type: string
deployment_env:
type: string
owner:
type: string
version:
type: string
status:
type: string
did:
type: string
description: W3C DID for this agent (if DID workstream is active)
created_at:
type: string
format: date-time
example:
sub: "agt_01HXK7Z9P3FKWABCDEF67890"
agent_id: "agt_01HXK7Z9P3FKWABCDEF67890"
agent_type: "orchestrator"
organization_id: "org_01HXK7Z9P3FKWABCDEF12345"
capabilities: ["task-planning", "tool-use"]
deployment_env: "production"
owner: "acme-ai"
version: "1.2.0"
status: "active"
did: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
created_at: "2026-03-29T12:00:00Z"
401 Unauthorized:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "UNAUTHORIZED"
message: "Invalid or expired access token"
Database Schema Changes
New Table: oidc_keys
Stores the RSA/EC key pairs used for ID token signing. Private keys stored in Vault; public key JWK in PostgreSQL for JWKS endpoint.
CREATE TABLE oidc_keys (
key_id VARCHAR(40) PRIMARY KEY,
kid VARCHAR(100) NOT NULL UNIQUE, -- Key ID in JWKS
algorithm VARCHAR(10) NOT NULL,
use_purpose VARCHAR(10) NOT NULL DEFAULT 'sig',
public_key_jwk JSONB NOT NULL,
vault_key_path VARCHAR(255) NOT NULL,
is_current BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
retired_at TIMESTAMPTZ,
CONSTRAINT oidc_keys_alg_check CHECK (algorithm IN ('RS256', 'ES256')),
CONSTRAINT oidc_keys_use_check CHECK (use_purpose IN ('sig', 'enc'))
);
CREATE INDEX idx_oidc_keys_is_current ON oidc_keys(is_current) WHERE is_current = TRUE;
Configuration
| Environment Variable | Description | Default |
|---|---|---|
OIDC_ISSUER |
OIDC issuer URL (must match token iss claim) |
https://${HOST} |
OIDC_ID_TOKEN_TTL_SECONDS |
ID token lifetime | 3600 |
OIDC_SIGNING_ALG |
ID token signing algorithm | RS256 |
OIDC_JWKS_CACHE_TTL_SECONDS |
JWKS response cache TTL | 3600 |
OIDC_KEY_ROTATION_DAYS |
Days between signing key rotations | 90 |
Dependencies
| Package | Version | Purpose |
|---|---|---|
oidc-provider |
^8.4.6 |
Certified OIDC server library (OpenID Foundation conformant) |
Security Considerations
- ID token signing keys are stored in Vault; public keys only are served via JWKS
- JWKS endpoint is cached in Redis (
OIDC_JWKS_CACHE_TTL_SECONDS) to prevent key-hammering - Key rotation: when a new signing key is created, the old key remains in JWKS until all tokens signed with it have expired
- The
openidscope is only issued to callers explicitly requesting it — not included by default GET /agent-inforeturns the same data as the ID token — no additional sensitive data- ID tokens for agent credentials must not contain client secrets or internal system paths
alg: noneis explicitly rejected — all ID tokens must be signed
Acceptance Criteria
/.well-known/openid-configurationpasses OIDC Discovery conformance validation/.well-known/jwks.jsonreturns valid JWKS with current signing public key- ID token returned when
openidscope is in token request; not returned otherwise - ID token is verifiable against JWKS endpoint using standard JWT libraries
- ID token claims match agent record (agent_type, capabilities, organization_id, did)
/agent-inforeturns correct claims for authenticated agent- Key rotation: old JWKS key is kept until all signed tokens expire
- TypeScript strict, zero
any, >80% test coverage on OIDCService