mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:11:31 +00:00
docs(arch-h1): Phase 13 Sprint 13.6 — OpenAPI batch 3 final 7 ops; rest-deferred bucket reaches 0
Phase 13 Sprint 13.6 — the FINAL ARCH-H1 OpenAPI authoring batch.
Closes the substantive burn-down: rest-deferred bucket reaches 0;
every REST-shaped router route is now authored into openapi.yaml.
Documented exceptions are exclusively wire-protocol contracts (SCEP
RFC 8894, ACME RFC 8555, ACME ARI RFC 9773, EST RFC 7030).
Sprint 13.7 next (closure / audit-HTML flip) tightens this commit's
floor: the rest-deferred bucket pin in
openapi-rest-deferred-monotonic.sh changes from
"monotonic-decrease vs baseline" to "hard zero-exact" so a future
PR adding a REST route MUST author its OpenAPI op or fail CI — the
`category: rest-deferred` escape hatch closes for good.
7 new operations (the final batch)
==================================
One-off REST endpoints (4 ops):
GET /api/v1/audit/export exportAudit (audit.export — NDJSON stream)
POST /api/v1/auth/demo-residual/cleanup cleanupDemoResidualGrants (auth.role.assign; 503 in demo mode)
POST /auth/logout logoutCurrentSession (auth-exempt; cookie checked inside)
POST /auth/breakglass/login breakglassLogin (auth-bypass; 404 when disabled; rate-limited)
OIDC browser-flow endpoints (3 ops, modeled as 302+Location-header
redirects per OAS 3.1 — `responses.302` + `headers.Location` +
description noting the server-initiated redirect contract; empty
content block; consumers must follow the redirect for the flow to
complete):
GET /auth/oidc/login oidcLoginInitiate (auth-exempt; 302 → IdP authz URL + pre-login cookie)
GET /auth/oidc/callback oidcLoginCallback (auth-exempt; 302 → postLoginURL on success / 302 → /login?error=oidc_failed&reason=<cat> on failure)
POST /auth/oidc/back-channel-logout oidcBackChannelLogout (auth via IdP-signed logout_token; 200 + Cache-Control: no-store on success; uniform 400 per spec §2.6 on failure)
The 4 one-off REST endpoints model standard JSON contracts. The 3
OIDC browser-flow endpoints DELIBERATELY model the 302-with-Location
contract because that's the live wire shape — modeling them as
200-with-JSON would lie about reality (and break any generated
client that assumes a JSON response body). Each `headers.Location`
is documented with the actual redirect target shape (provider authz
URL / postLoginURL / /login?error=oidc_failed&reason=<category>).
Audit/export NDJSON streaming
=============================
The audit/export response is `application/x-ndjson` — one JSON-
encoded AuditEvent per line, NOT a single JSON document. Documented
explicitly so generated clients know to parse line-by-line. Schema
references the existing #/components/schemas/AuditEvent (already
defined as part of the audit-events surface).
Range cap + per-record cap + filter shape all documented in the
parameters block (90-day max window, 1..100000 limit, category enum
of cert_lifecycle/auth/config).
2 new schemas (components/schemas)
==================================
DemoResidualCleanupResponse — mirrors demoResidualCleanupResponse
({removed: int64}).
BreakglassLoginRequest — mirrors breakglassLoginRequest
(actor_id + password; password
marked `format: password`).
Pre-existing AuditEvent + BreakglassLoginRequest-adjacent schemas
(Sprint 13.4 + 13.5) are referenced via $ref without duplication.
Exception YAML + baseline + zero-floor pin
==========================================
7 entries removed from api/openapi-handler-exceptions.yaml. Post-cut
shape:
total entries: 36
wire-protocol: 36 (unchanged — these never burn down)
rest-deferred: 0 ← THE FLOOR
Baseline file bumped 7 → 0. The Sprint 13.1 monotonic-decrease
guard now pins `rest-deferred ≤ 0` — equivalent to "the bucket
must stay empty." Sprint 13.7 will additionally tighten the
parity-script's missing-category check so the bucket can't be
re-grown via the `category:` typo escape hatch either.
YAML header narrative updated: "Sprint 13.6 SHIPPED — 7 - 7 = 0".
ARCH-H1 substantive close achieved at the bucket-math level.
Receipts (all from the live tree)
=================================
$ grep -cE '^\s+operationId:' api/openapi.yaml
186 (was 179 + 7)
$ bash scripts/ci-guards/openapi-handler-parity.sh
Router routes: 220
OpenAPI operations: 186
Documented exceptions: 36
wire-protocol: 36
rest-deferred: 0
openapi-handler-parity: clean.
$ bash scripts/ci-guards/openapi-rest-deferred-monotonic.sh
openapi-rest-deferred-monotonic: clean — rest-deferred = 0,
baseline = 0.
$ cat api/openapi-handler-exceptions-baseline.txt
0
$ python3 -c "import yaml; ..."
paths: 140, operations: 186, schemas: 74
sprint-13.6 schemas missing: (none)
OpenAPI lint: clean.
$ gofmt -l . → clean
$ go vet ./internal/api/handler/... ./cmd/server/... → clean
ARCH-H1 final tally (across Sprints 13.1 + 13.4 + 13.5 + 13.6)
==============================================================
Sprint 13.1: structural categorization — split 64 exceptions into
36 wire-protocol + 28 rest-deferred; added parity-
script bucket reporting + monotonic-decrease guard +
baseline file. ARCH-H1's structural close.
Sprint 13.4: 13 OpenAPI ops + 13 exception deletions + baseline
28 → 15. Auth/sessions + OIDC CRUD/JWKS/test/refresh
+ group-mappings clusters.
Sprint 13.5: 8 OpenAPI ops + 8 exception deletions + baseline
15 → 7. Auth/breakglass + auth/users +
auth/runtime-config clusters.
Sprint 13.6 (this commit): 7 OpenAPI ops + 7 exception deletions
+ baseline 7 → 0. Audit/export + demo-residual +
auth/logout + auth/breakglass/login + 3 OIDC browser
flows. ARCH-H1's substantive close.
Cumulative: 28 OpenAPI ops authored, 28 exception entries deleted,
rest-deferred bucket drained from 28 → 0. The OpenAPI surface
exactly matches every REST-shaped router route.
Sprint 13.7 closes the audit HTML flip + tightens this commit's
monotonic-decrease floor to a zero-exact pin so the burn-down is
locked.
Refs: ARCH-H1 substantive close — final batch.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
7
|
0
|
||||||
|
|||||||
@@ -41,13 +41,13 @@
|
|||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Current split, re-derived by the parity script's bucket-reporting
|
# Current split, re-derived by the parity script's bucket-reporting
|
||||||
# subcommand (post-Sprint-13.5 / 2026-05-14):
|
# subcommand (post-Sprint-13.6 / 2026-05-14):
|
||||||
#
|
#
|
||||||
# total entries: 43
|
# total entries: 36
|
||||||
# wire-protocol: 36
|
# wire-protocol: 36
|
||||||
# rest-deferred: 7
|
# rest-deferred: 0 ← THE FLOOR — ARCH-H1 substantive close
|
||||||
#
|
#
|
||||||
# Burn-down progress + remaining plan for the rest-deferred bucket:
|
# Burn-down progress:
|
||||||
#
|
#
|
||||||
# Sprint 13.4 SHIPPED — 28 - 13 = 15 (auth/sessions cluster 3 ops +
|
# Sprint 13.4 SHIPPED — 28 - 13 = 15 (auth/sessions cluster 3 ops +
|
||||||
# auth/oidc CRUD + JWKS + test + refresh
|
# auth/oidc CRUD + JWKS + test + refresh
|
||||||
@@ -55,13 +55,15 @@
|
|||||||
# Sprint 13.5 SHIPPED — 15 - 8 = 7 (auth/breakglass admin 4 ops +
|
# Sprint 13.5 SHIPPED — 15 - 8 = 7 (auth/breakglass admin 4 ops +
|
||||||
# auth/users 3 ops + auth/runtime-config
|
# auth/users 3 ops + auth/runtime-config
|
||||||
# 1 op, 8 ops total)
|
# 1 op, 8 ops total)
|
||||||
# Sprint 13.6 → 7 - 7 = 0 (audit/export + demo-residual + 3
|
# Sprint 13.6 SHIPPED — 7 - 7 = 0 (audit/export 1 op + demo-
|
||||||
# OIDC browser flows + auth/logout +
|
# residual/cleanup 1 op + auth/logout 1 op +
|
||||||
# auth/breakglass/login, 7 ops)
|
# auth/breakglass/login 1 op + 3 OIDC
|
||||||
|
# browser-flow endpoints, 7 ops total)
|
||||||
#
|
#
|
||||||
# Sprint 13.7 then tightens the parity-script's rest-deferred floor
|
# Sprint 13.7 next tightens the parity-script's rest-deferred floor
|
||||||
# from monotonic-decrease to a hard zero-exact pin. After that, any
|
# from monotonic-decrease to a hard zero-exact pin. After that, any
|
||||||
# new REST route MUST land with an OpenAPI op or fail CI.
|
# new REST route MUST land with an OpenAPI op or fail CI — no escape
|
||||||
|
# hatch via `category: rest-deferred`.
|
||||||
#
|
#
|
||||||
# Each authored OpenAPI op needs request/response schemas (not
|
# Each authored OpenAPI op needs request/response schemas (not
|
||||||
# placeholders) so the generated client at web/orval.config.ts emits
|
# placeholders) so the generated client at web/orval.config.ts emits
|
||||||
@@ -197,24 +199,3 @@ documented_exceptions:
|
|||||||
# stays green for the v2.1.0 release tag. Threat model + handler contracts
|
# stays green for the v2.1.0 release tag. Threat model + handler contracts
|
||||||
# live in docs/operator/{rbac.md,auth-threat-model.md,oidc-runbooks/*}.
|
# live in docs/operator/{rbac.md,auth-threat-model.md,oidc-runbooks/*}.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
- route: "GET /auth/oidc/login"
|
|
||||||
why: "Bundle 2 Phase 5 OIDC login redirect; user-facing 302 with state cookie. OpenAPI rep deferred to pre-2.2.0."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "GET /auth/oidc/callback"
|
|
||||||
why: "Bundle 2 Phase 5 OIDC callback handler; RFC 9700 §4.7.1 + RFC 9207. OpenAPI rep deferred to pre-2.2.0."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "POST /auth/logout"
|
|
||||||
why: "Bundle 2 Phase 5 cookie + CSRF revoker. OpenAPI rep deferred to pre-2.2.0."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "POST /auth/breakglass/login"
|
|
||||||
why: "Bundle 2 Phase 7.5 public break-glass login (auth-bypass, 404 when disabled). OpenAPI rep deferred to pre-2.2.0."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "POST /auth/oidc/back-channel-logout"
|
|
||||||
why: "Bundle 2 Phase 5 RFC OIDC Back-Channel Logout 1.0 endpoint. OpenAPI rep deferred to pre-2.2.0."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "POST /api/v1/auth/demo-residual/cleanup"
|
|
||||||
why: "Audit 2026-05-11 A-8 demo-mode residual-grants cleanup endpoint."
|
|
||||||
category: rest-deferred
|
|
||||||
- route: "GET /api/v1/audit/export"
|
|
||||||
why: "Bundle 1 Phase 8 streaming NDJSON audit export."
|
|
||||||
category: rest-deferred
|
|
||||||
|
|||||||
@@ -1217,6 +1217,313 @@ paths:
|
|||||||
"404": { description: User not found }
|
"404": { description: User not found }
|
||||||
"500": { description: Internal error }
|
"500": { description: Internal error }
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════
|
||||||
|
# Phase 13 Sprint 13.6 (ARCH-H1 batch 3 — the final batch).
|
||||||
|
# 4 one-off REST endpoints + 3 OIDC browser-flow endpoints; rest-
|
||||||
|
# deferred bucket goes from 7 → 0 with this commit. Browser-flow
|
||||||
|
# endpoints model the 302+Location-header redirect contract per
|
||||||
|
# OAS 3.1; consumers know they are NOT standard JSON responses.
|
||||||
|
# ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/api/v1/audit/export:
|
||||||
|
get:
|
||||||
|
tags: [Audit]
|
||||||
|
summary: Export audit events as newline-delimited JSON (NDJSON) for a date range
|
||||||
|
description: |
|
||||||
|
Permission `audit.export`. Streams every audit row inside the
|
||||||
|
requested `[from, to]` window as `application/x-ndjson`. Used
|
||||||
|
by compliance pipelines (Splunk Universal Forwarder, Elastic
|
||||||
|
Filebeat, Vector, etc.) that prefer line-by-line ingestion
|
||||||
|
over a single JSON document.
|
||||||
|
|
||||||
|
Range cap: 90 days. Requests with `to - from > 90d` return
|
||||||
|
400; paginate by narrower windows.
|
||||||
|
|
||||||
|
Per-record cap: `limit` query parameter (default 50000;
|
||||||
|
accepted range 1..100000). Values outside the range silently
|
||||||
|
clamp to default.
|
||||||
|
|
||||||
|
The export itself is recursively audited: every successful
|
||||||
|
export emits an `audit.export` event capturing actor, range,
|
||||||
|
category, and row count so the audit log records who pulled
|
||||||
|
which compliance evidence and when.
|
||||||
|
operationId: exportAudit
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: from
|
||||||
|
required: true
|
||||||
|
schema: { type: string, format: date-time }
|
||||||
|
description: RFC 3339 start of the export window (inclusive).
|
||||||
|
- in: query
|
||||||
|
name: to
|
||||||
|
required: true
|
||||||
|
schema: { type: string, format: date-time }
|
||||||
|
description: RFC 3339 end of the export window (exclusive). Must be strictly after `from`.
|
||||||
|
- in: query
|
||||||
|
name: category
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [cert_lifecycle, auth, config]
|
||||||
|
description: Optional category filter. Omit to return every event in the window.
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
required: false
|
||||||
|
schema: { type: integer, minimum: 1, maximum: 100000 }
|
||||||
|
description: Maximum rows to stream (default 50000; out-of-range values clamp to default).
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: NDJSON stream of AuditEvent rows
|
||||||
|
headers:
|
||||||
|
Content-Disposition:
|
||||||
|
description: Attachment hint suggesting filename `certctl-audit-<from>_to_<to>.ndjson`.
|
||||||
|
schema: { type: string }
|
||||||
|
content:
|
||||||
|
application/x-ndjson:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
One JSON-encoded AuditEvent per line (see
|
||||||
|
`#/components/schemas/AuditEvent`). The body is a
|
||||||
|
byte stream, not a single JSON document; clients
|
||||||
|
parse line-by-line.
|
||||||
|
"400": { description: Missing/invalid date params, range exceeds 90 days, invalid category, etc. }
|
||||||
|
"401": { description: Unauthorized }
|
||||||
|
"403": { description: Forbidden }
|
||||||
|
"405": { description: Method not allowed (only GET is accepted) }
|
||||||
|
"500": { description: Internal error (export failed mid-query) }
|
||||||
|
|
||||||
|
/api/v1/auth/demo-residual/cleanup:
|
||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Remove residual `actor-demo-anon` role grants after exiting demo mode
|
||||||
|
description: |
|
||||||
|
Permission `auth.role.assign` (admin-class). Removes the
|
||||||
|
leftover `actor_roles` rows for the synthetic `actor-demo-anon`
|
||||||
|
actor after the operator has flipped `CERTCTL_AUTH_TYPE` away
|
||||||
|
from `none` (demo mode). Idempotent: subsequent invocations
|
||||||
|
return `removed: 0`.
|
||||||
|
|
||||||
|
Refuses (503) when the server is currently in demo mode —
|
||||||
|
the `actor-demo-anon` grants ARE the active runtime state at
|
||||||
|
`auth_type=none`, so "cleaning them up" would lock the operator
|
||||||
|
out of the live admin surface. The GUI hides the action button
|
||||||
|
when `/api/v1/auth/info` reports `auth_type=none`; this guard
|
||||||
|
is defense-in-depth.
|
||||||
|
operationId: cleanupDemoResidualGrants
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Cleanup complete (count returned)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/DemoResidualCleanupResponse"
|
||||||
|
"401": { description: Unauthorized }
|
||||||
|
"403": { description: Forbidden }
|
||||||
|
"500": { description: Internal error (cleanup not configured or failed mid-delete) }
|
||||||
|
"503": { description: Refused — server is currently in demo mode (CERTCTL_AUTH_TYPE=none) }
|
||||||
|
|
||||||
|
/auth/logout:
|
||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Revoke the caller's current session
|
||||||
|
description: |
|
||||||
|
Auth-exempt at the router (the session cookie is validated
|
||||||
|
inside the handler). Reads `certctl_session` cookie, validates
|
||||||
|
+ revokes the underlying session row, rotates the CSRF token
|
||||||
|
on the actor's other sessions (Audit 2026-05-11 Fix 13 / HIGH-2
|
||||||
|
fourth call site), clears the session + CSRF cookies, returns
|
||||||
|
204.
|
||||||
|
|
||||||
|
Idempotent: when no valid session cookie is presented the
|
||||||
|
handler clears any stale cookies and returns 204 without
|
||||||
|
side-effects.
|
||||||
|
operationId: logoutCurrentSession
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"204": { description: Session revoked (or none to revoke — idempotent) }
|
||||||
|
"500": { description: Internal error during revoke }
|
||||||
|
|
||||||
|
/auth/breakglass/login:
|
||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Local password login for emergency admin recovery
|
||||||
|
description: |
|
||||||
|
Auth-bypass — the whole point is to log in WITHOUT existing
|
||||||
|
certctl credentials. On success, sets the post-login session
|
||||||
|
cookie + CSRF cookie and returns 204. On any failure (wrong
|
||||||
|
password, locked account, no credential, unknown actor):
|
||||||
|
uniform 401 + identical timing (no scanner-friendly
|
||||||
|
distinction).
|
||||||
|
|
||||||
|
Returns 404 when `CERTCTL_BREAKGLASS_ENABLED=false` (surface
|
||||||
|
invisibility — Phase 7.5 spec).
|
||||||
|
|
||||||
|
Rate-limited per source IP (default 5 attempts/min/IP;
|
||||||
|
configurable via `CERTCTL_RATE_LIMIT_BACKEND` + the
|
||||||
|
constructor in cmd/server/main.go). Exceeded budget returns
|
||||||
|
429 with no body.
|
||||||
|
operationId: breakglassLogin
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/BreakglassLoginRequest"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Authenticated — Set-Cookie carries the new session + CSRF tokens
|
||||||
|
headers:
|
||||||
|
Set-Cookie:
|
||||||
|
description: |
|
||||||
|
Two `Set-Cookie` headers are emitted on success: the
|
||||||
|
post-login session cookie (`certctl_session`, HttpOnly)
|
||||||
|
and the CSRF token cookie (`certctl_csrf`, NOT HttpOnly
|
||||||
|
so the GUI can read it to echo in `X-CSRF-Token`).
|
||||||
|
schema: { type: string }
|
||||||
|
"401": { description: Authentication failed (uniform — covers wrong password, missing credential, unknown actor; identical timing) }
|
||||||
|
"404": { description: Break-glass disabled (CERTCTL_BREAKGLASS_ENABLED=false) }
|
||||||
|
"429": { description: Rate limit exceeded (per source IP) }
|
||||||
|
"500": { description: Internal error during authenticate }
|
||||||
|
|
||||||
|
/auth/oidc/login:
|
||||||
|
get:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Browser-flow — start an OIDC login; 302 to IdP authorization URL
|
||||||
|
description: |
|
||||||
|
Auth-exempt — pre-auth by definition. Browser-flow endpoint:
|
||||||
|
the response is a 302 with `Location:` pointing at the
|
||||||
|
configured provider's authorization URL, NOT a JSON response.
|
||||||
|
Consumers MUST follow the redirect for the flow to complete.
|
||||||
|
|
||||||
|
On success: persists a pre-login row capturing the state +
|
||||||
|
nonce + PKCE-S256 verifier, sets the `certctl_oidc_pending`
|
||||||
|
cookie (HttpOnly, SameSite=Lax, 10-minute lifetime, `__Host-`
|
||||||
|
prefix), and 302-redirects to the IdP. The cookie is consumed
|
||||||
|
+ cleared by `/auth/oidc/callback`.
|
||||||
|
|
||||||
|
Audit 2026-05-10 MED-16 — the pre-login row captures the
|
||||||
|
client IP + User-Agent at this step so the callback handler
|
||||||
|
can reject a stolen cookie replayed from a different browser
|
||||||
|
or source.
|
||||||
|
operationId: oidcLoginInitiate
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: provider
|
||||||
|
required: true
|
||||||
|
schema: { type: string }
|
||||||
|
description: OIDC provider ID (one of the rows under `/api/v1/auth/oidc/providers`).
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Redirect to the IdP's authorization URL
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
description: IdP authorization URL (provider-specific, includes state + nonce + PKCE challenge).
|
||||||
|
schema: { type: string, format: uri }
|
||||||
|
Set-Cookie:
|
||||||
|
description: The `certctl_oidc_pending` pre-login cookie.
|
||||||
|
schema: { type: string }
|
||||||
|
"400": { description: Missing `provider` query parameter }
|
||||||
|
"404": { description: Provider not found }
|
||||||
|
"500": { description: IdP discovery fetch failed, downgrade-defense tripped, or internal crypto failure }
|
||||||
|
|
||||||
|
/auth/oidc/callback:
|
||||||
|
get:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Browser-flow — consume IdP authorization response; 302 to post-login URL
|
||||||
|
description: |
|
||||||
|
Auth-exempt — pre-auth by definition (the cookie + state are
|
||||||
|
validated inside the handler). Browser-flow endpoint: the
|
||||||
|
success response is a 302 with `Location:` pointing at the
|
||||||
|
configured `postLoginURL` (default `/`), NOT a JSON response.
|
||||||
|
|
||||||
|
Reads the `certctl_oidc_pending` pre-login cookie, drives the
|
||||||
|
OIDC service's 11-step token validation (sig + iss + aud +
|
||||||
|
nonce + at_hash + iat + jti + sub + group-claim resolution +
|
||||||
|
role-mapping + user-upsert), mints a post-login session,
|
||||||
|
deletes the pre-login cookie, sets the post-login session +
|
||||||
|
CSRF cookies, and 302's to the dashboard.
|
||||||
|
|
||||||
|
Failure responses are ALSO 302 — to `/login?error=oidc_failed&reason=<category>`
|
||||||
|
— so the SPA can render an operator-friendly alert without
|
||||||
|
the browser seeing a raw 400. The audit row carries the
|
||||||
|
specific failure category server-side.
|
||||||
|
operationId: oidcLoginCallback
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: code
|
||||||
|
required: true
|
||||||
|
schema: { type: string }
|
||||||
|
description: OAuth2 authorization code returned by the IdP.
|
||||||
|
- in: query
|
||||||
|
name: state
|
||||||
|
required: true
|
||||||
|
schema: { type: string }
|
||||||
|
description: Opaque state value the certctl `/auth/oidc/login` step embedded into the IdP URL.
|
||||||
|
- in: query
|
||||||
|
name: iss
|
||||||
|
required: false
|
||||||
|
schema: { type: string }
|
||||||
|
description: RFC 9207 `iss` URL parameter. Preserved byte-strict for the service-layer compare against the matched provider's `IssuerURL`. The IdP emits this only when advertised in its discovery doc; the service-layer check is a no-op otherwise.
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Redirect — either to `postLoginURL` on success OR to `/login?error=oidc_failed&reason=<category>` on validation failure
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
description: Either the configured `postLoginURL` (success) or `/login?error=oidc_failed&reason=<category>` (failure).
|
||||||
|
schema: { type: string, format: uri-reference }
|
||||||
|
Set-Cookie:
|
||||||
|
description: On success — two `Set-Cookie` headers carrying the post-login session + CSRF tokens. On failure — a single `Set-Cookie` clearing the pre-login cookie.
|
||||||
|
schema: { type: string }
|
||||||
|
"400": { description: Missing `code`/`state` query parameter, or missing pre-login cookie }
|
||||||
|
|
||||||
|
/auth/oidc/back-channel-logout:
|
||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: IdP-initiated session revocation via OIDC Back-Channel Logout 1.0
|
||||||
|
description: |
|
||||||
|
Auth via the IdP-signed `logout_token` JWT in the form body —
|
||||||
|
NOT certctl-issued credentials, so `security: []`. Spec
|
||||||
|
reference: OpenID Connect Back-Channel Logout 1.0 §2.6.
|
||||||
|
|
||||||
|
Validates the logout token against the matched provider's
|
||||||
|
JWKS (signature + alg-allowlist + iat-skew window + jti
|
||||||
|
consumed-set + required claims: iss, aud, iat, jti, events;
|
||||||
|
exactly one of sub or sid; nonce MUST be absent), revokes
|
||||||
|
every matching session, returns 200 with `Cache-Control:
|
||||||
|
no-store`.
|
||||||
|
|
||||||
|
Any validation failure returns 400 — uniform wire shape per
|
||||||
|
spec §2.6. The audit row carries the specific reason.
|
||||||
|
Replayed jti (RFC 9700 §2.7) returns 200 with audit
|
||||||
|
outcome=jti_replayed (idempotent re-receive of a logout
|
||||||
|
is harmless).
|
||||||
|
operationId: oidcBackChannelLogout
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [logout_token]
|
||||||
|
properties:
|
||||||
|
logout_token:
|
||||||
|
type: string
|
||||||
|
description: IdP-signed logout_token JWT per OpenID Connect Back-Channel Logout 1.0 §2.4.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Logout token accepted; matching sessions revoked
|
||||||
|
headers:
|
||||||
|
Cache-Control:
|
||||||
|
description: Always `no-store` per spec.
|
||||||
|
schema: { type: string, enum: [no-store] }
|
||||||
|
"400": { description: logout_token validation failed (signature, iss, aud, iat-window, jti consumed-set, required claims, etc.) — uniform per spec §2.6 }
|
||||||
|
"503": { description: Transient repository error on jti consumed-set write — IdP should retry per its own semantics }
|
||||||
|
|
||||||
/api/v1/auth/runtime-config:
|
/api/v1/auth/runtime-config:
|
||||||
get:
|
get:
|
||||||
tags: [Auth]
|
tags: [Auth]
|
||||||
@@ -7411,3 +7718,37 @@ components:
|
|||||||
format: date-time
|
format: date-time
|
||||||
nullable: true
|
nullable: true
|
||||||
description: RFC 3339 UTC timestamp the user was deactivated. Omitted when the user is active.
|
description: RFC 3339 UTC timestamp the user was deactivated. Omitted when the user is active.
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════════════
|
||||||
|
# Phase 13 Sprint 13.6 (ARCH-H1 batch 3 — final batch) schemas.
|
||||||
|
# ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DemoResidualCleanupResponse:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Mirrors internal/api/handler/demo_residual.go::
|
||||||
|
demoResidualCleanupResponse. Always present; idempotent re-runs
|
||||||
|
return `removed: 0`.
|
||||||
|
required: [removed]
|
||||||
|
properties:
|
||||||
|
removed:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: Number of `actor_roles` rows removed in this cleanup call.
|
||||||
|
|
||||||
|
BreakglassLoginRequest:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Mirrors internal/api/handler/auth_breakglass.go::
|
||||||
|
breakglassLoginRequest. Plaintext password on the wire ONLY at
|
||||||
|
login-time; the service hashes via Argon2id for the
|
||||||
|
constant-time compare.
|
||||||
|
required: [actor_id, password]
|
||||||
|
properties:
|
||||||
|
actor_id:
|
||||||
|
type: string
|
||||||
|
description: Actor attempting recovery login.
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: Plaintext password (Argon2id-hashed at rest by the service).
|
||||||
|
|||||||
Reference in New Issue
Block a user