Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions charts/insight/templates/secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ stringData:
# Audit events to the platform Redpanda topic (step 10.8). Empty = disabled
# (structured log only) — the global redpanda.brokers wires it when set.
APP__gears__authenticator__config__audit__brokers: {{ .Values.redpanda.brokers | default "" | quote }}
{{- if .Values.authenticator.overrideEnabled }}
# `__override` view-as login (#1941) — dev/demo environments ONLY.
APP__gears__authenticator__config__override_enabled: "true"
{{- end }}

{{- if .Values.identity.deploy }}
---
Expand Down
4 changes: 4 additions & 0 deletions charts/insight/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ authenticator:
clientId: "insight-authenticator"
clientSecret: ""
redirectUri: "" # browser-facing callback, through the gateway edge
# Honor `/auth/login?__override=<email>` (view-as, #1941): the session is
# minted for that person instead of the authenticated one. Dev/demo
# environments ONLY — MUST stay false anywhere real users log in.
overrideEnabled: false
# ─── fakeidp (dev/e2e OIDC provider — NEVER a real environment) ─────────────
fakeidp:
deploy: false
Expand Down
17 changes: 15 additions & 2 deletions docs/components/backend/authenticator/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ date: 2026-07-06
- [DD-AUTH-06: Two Listeners for Two Internal Surfaces](#dd-auth-06-two-listeners-for-two-internal-surfaces)
- [DD-AUTH-07: Access-Control Claims Fetched Once, at Login](#dd-auth-07-access-control-claims-fetched-once-at-login)
- [DD-AUTH-08: Empty-Table First-Admin Bootstrap plus INSTALLER](#dd-auth-08-empty-table-first-admin-bootstrap-plus-installer)
- [DD-AUTH-09: View-As Override at Session Mint, Gated by an Environment Flag](#dd-auth-09-view-as-override-at-session-mint-gated-by-an-environment-flag)
- [RESOLVED (step 07): ES256 for the Gateway JWT](#resolved-step-07-es256-for-the-gateway-jwt)
- [6. Traceability](#6-traceability)

Expand Down Expand Up @@ -84,6 +85,7 @@ The authenticator is a plain HTTP service: no proxying, no K8s API access, no st
| `cpt-insightspec-fr-auth-service-tokens` | Token listener + RFC 7523 verification against the registry; same signer, same JWKS |
| `cpt-insightspec-fr-auth-bootstrap` | Empty-table guard in the login path; INSTALLER as the production path |
| `cpt-insightspec-fr-auth-internal-reachability` | Two listeners (3.10); NetworkPolicies; credential checks on every endpoint |
| `cpt-insightspec-fr-auth-override` | View-as swap at session mint, gated by default-off `override_enabled` (DD-AUTH-09); `impersonator_*` fields in the session record; real principal audited |

#### NFR Allocation

Expand Down Expand Up @@ -548,7 +550,7 @@ sequenceDiagram
B-->>U: Set-Cookie __Host-sid=(session token) + 302 to SPA
```

**Description**: The only moment IdP tokens are exchanged. The session-fixation guard (revoke any live session named by an incoming cookie, always generate the new token server-side) runs before session creation, exactly as in the deleted BFF spec.
**Description**: The only moment IdP tokens are exchanged. The session-fixation guard (revoke any live session named by an incoming cookie, always generate the new token server-side) runs before session creation, exactly as in the deleted BFF spec. When `override_enabled` and the login carried `__override=<email>`, the effective person is swapped here — after IdP authentication and person resolution, before session creation (DD-AUTH-09).

#### Every API request -- cookie in, JWT out

Expand Down Expand Up @@ -754,6 +756,8 @@ graph LR
| `user_agent` | String | Captured at login |
| `ip` | String | Captured at login |
| `csrf_token` | String | CSRF token bound to this session |
| `impersonator_person_id` | String | Real principal behind a `__override` view-as session (DD-AUTH-09); empty on normal logins |
| `impersonator_email` | String | Real principal's email on a view-as session; surfaced by `/auth/me` |

**Redis TTL**: matches `expires_at`; re-set on every refresh.

Expand All @@ -779,7 +783,7 @@ graph LR

#### Key: `asm:login_state:{state}`

**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`. **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Field name mismatch: redirect_to vs. actual return_to.

LoginState in session.rs names the field return_to (used in to_fields()/from_map()), not redirect_to.

📝 Proposed fix
-**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
+**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `return_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `redirect_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
**Type**: Redis HASH. Fields: `pkce_verifier`, `nonce`, `return_to`, `override_email` (view-as target, DD-AUTH-09; empty on normal logins). **TTL**: 5 minutes, one-shot. The live count is capped (layer-2 rate limiting).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/components/backend/authenticator/DESIGN.md` at line 786, Update the
Redis HASH field list in the LoginState documentation to use `return_to` instead
of `redirect_to`, matching the field serialized by `LoginState::to_fields()` and
parsed by `LoginState::from_map()`; leave the other fields unchanged.


#### Key: `asm:logout_jti:{iss}:{jti}`

Expand Down Expand Up @@ -878,6 +882,7 @@ Step-10 additions (all defaulted; the config struct mirrors them 1:1):
| `authenticator.idp.refresh_due_jitter_seconds` | `30` | ± jitter applied to due-times when written (anti-herding, G5). |
| `authenticator.rate_limit.login_state_max` | `1000` | Cap on concurrent live login states; excess `/auth/login` → 429. |
| `authenticator.rate_limit.refresh_burst` / `refresh_per_minute` | `5` / `6` | Per-session `/auth/refresh` token bucket (burst 0 disables). |
| `authenticator.override_enabled` | `false` | Honor `__override=<email>` on `/auth/login` (view-as, DD-AUTH-09). Dev/demo environments ONLY — never set where real users log in. |
| `authenticator.rate_limit.callback_burst` / `callback_per_minute` | `5` / `10` | Per-state `/auth/callback` token bucket. |
| `authenticator.audit.brokers` | `""` | Redpanda bootstrap servers for audit events; empty = disabled (structured log only). |
| `authenticator.audit.topic` | `insight.audit.events` | The platform audit topic. |
Expand Down Expand Up @@ -1089,6 +1094,14 @@ Recorded here so the decisions survive the deleted tree; rationale as originally

**Consequences**: The documented race ("first colleague to log in wins the universe") is bounded to IdP-authenticated principals on an empty install and is loudly audited; security-sensitive installs disable it.

### DD-AUTH-09: View-As Override at Session Mint, Gated by an Environment Flag

**Decision**: Restore the operator "view the dashboard as another user" facility (`?__override=<email>`, issue #1941) inside the authenticator, at the only legitimate seam: the target email is stored with the transient login state at `/auth/login` and applied at `/auth/callback` — after full IdP authentication and person resolution — by resolving the target through the same Identity lookup and minting the session + linked JWT for the target. Gated by a single `override_enabled` flag, default `false`, wired through Helm; no per-user allowlist.

**Why**: The gateway hardening made viewer identity exclusively gateway-authored (inbound `X-Insight-*` stripped), which correctly killed the old client-side override; the replacement must therefore act where identity is authored. All decision inputs are server-side (flag, login-state value, person store) — the parameter itself is never trusted as identity, so the spoofing hole stays closed. A per-user allowlist is deliberately absent: roles are `default_roles` until the permissions service exists, so the flag marks the whole *environment* (dev/demo) as impersonation-capable instead of pretending to per-user authorization we cannot yet enforce. Once DD-AUTH-07 lands, the gate can become a role check without changing the flow.

**Consequences**: The session behaves as the target everywhere downstream (JWT `sub`, scope, session index) — that is the point. The record keeps `impersonator_*` fields and the real `idp_sub`/`idp_sid`/refresh token, so audit attribution, back-channel logout, and the background refresher keep targeting the real principal's IdP grant; the session is additionally indexed under the impersonator's `asm:user_sessions:*` (scored at the absolute cap — rotation only re-scores the target's index) so revoke-by-person against the real principal reaches it, and the real principal may list/revoke it as their own. `/auth/me` exposes `impersonator_email` for the SPA's "viewing as" banner. An unknown target is denied 403 (audited to the durable sink, not just the log), never silently ignored. Two accepted sharp edges, bounded by flag-on environments holding no real users: the Identity lookup is email-only (no tenant memberships until #1687), so a target from another tenant resolves and is paired with the caller's tenant claim; and compromise of any account grants impersonation of any known person. Both are why the default is `false` at every layer.

### RESOLVED (step 07): ES256 for the Gateway JWT

**Decision: mint ES256 (ECDSA P-256 / SHA-256).** Downstream verification (step 07) is done by the **upstream `cf-gears-oidc-authn-plugin`**, which resolves the JWKS through `jsonwebtoken`'s EC-capable `jwk::JwkSet` — so it validates EC keys natively. ES256 is the right default: 64-byte signatures and fast verification, and the gateway JWT rides in an HTTP header on **every** downstream request, so those bytes and cycles are paid per call. `none` is always rejected; the plugin's `supported_algorithms` is pinned to `["ES256"]`.
Expand Down
23 changes: 21 additions & 2 deletions docs/components/backend/authenticator/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ date: 2026-07-06
- [5.13 Service Tokens](#513-service-tokens)
- [5.14 Bootstrap of a Fresh Install](#514-bootstrap-of-a-fresh-install)
- [5.15 Internal Endpoint Reachability](#515-internal-endpoint-reachability)
- [5.16 View-As Override](#516-view-as-override)
- [6. Non-Functional Requirements](#6-non-functional-requirements)
- [6.1 NFR Inclusions](#61-nfr-inclusions)
- [6.2 NFR Exclusions](#62-nfr-exclusions)
Expand Down Expand Up @@ -460,6 +461,22 @@ Compose/dev has no NetworkPolicies; acceptable -- layers 1 and 3 still hold ther

**Actors**: `cpt-insightspec-actor-nginx-gateway`, `cpt-insightspec-actor-downstream-service`

### 5.16 View-As Override

#### `__override` Login Parameter (Dev/Demo Environments Only)

- [x] `p2` - **ID**: `cpt-insightspec-fr-auth-override`

The system **MUST** honor a `__override=<email>` query parameter on `GET /auth/login` (the historical portal name for the operator "view the dashboard as another user" facility) **only** when `authenticator.override_enabled` is set, and that flag **MUST** default to `false` at every layer (gear config, Helm values). With the flag off the parameter is inert: the login proceeds as the caller and the attempt is logged.

When enabled, the target email is stored server-side with the transient login state and applied only on the callback, **after** the caller completed a full IdP authentication and resolved to a known person: the target is resolved through the same Identity lookup, and the session and its linked JWT are minted for the target `person_id`/email. Note the lookup is **email-only** — Identity carries no tenant memberships yet (#1687) — so a target from another tenant resolves too, paired with the *caller's* tenant claim; acceptable while the flag marks whole dev/demo environments, to be revisited when membership resolution exists. The session record **MUST** retain the real principal (impersonator person id + email), the session **MUST** be revocable through the real principal as well (indexed under both persons), the login audit event **MUST** attribute the real principal and name the override target (denials included), and `/auth/me` **MUST** surface the impersonator so the SPA can display a "viewing as" indicator. An override naming an unknown person **MUST** be denied (no fallback to the caller's own identity — a typo must not look like the feature regressed).

Viewer identity remains exclusively gateway-authored: no client-supplied header or parameter is ever trusted as identity, so this does not reopen the header-spoofing hole the gateway hardening closed.

**Rationale**: Operators of dev/demo installs need to see the portal exactly as a given person (their scope, their metrics); marking the *environment* as impersonation-capable via a default-off flag keeps the facility out of every real deployment without per-user authorization machinery that does not exist yet (roles are `default_roles` until the permissions service lands).

**Actors**: `cpt-insightspec-actor-browser-user`, `cpt-insightspec-actor-oidc-provider`

## 6. Non-Functional Requirements

### 6.1 NFR Inclusions
Expand Down Expand Up @@ -534,11 +551,11 @@ If Redis is unreachable, `/internal/authz` and `/auth/*` mutations **MUST** fail

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/auth/login` | Start OIDC flow; 302 to IdP. |
| GET | `/auth/login` | Start OIDC flow; 302 to IdP. Optional `__override=<email>` view-as target, honored only when `override_enabled` (5.16). |
| GET | `/auth/callback` | OIDC callback; creates session + linked JWT; sets cookie; 302 to SPA. |
| POST | `/auth/refresh` | Rotate cookie, extend session TTL; return `{expires_at, refresh_at}`. |
| POST | `/auth/logout` | Revoke current session; clear cookie; return RP-logout URL. |
| GET | `/auth/me` | Current user, tenants, plus `{expires_at, refresh_at}`. |
| GET | `/auth/me` | Current user, tenants, plus `{expires_at, refresh_at}`; `impersonator_email` on view-as sessions (5.16). |
| GET | `/auth/sessions` | List active sessions for current user. |
| DELETE | `/auth/sessions/{id}` | Revoke a specific session. |
| DELETE | `/auth/sessions` | Revoke all sessions of current user. Admin/service variant revokes by user id (gateway-JWT authenticated). |
Expand Down Expand Up @@ -727,6 +744,7 @@ All endpoints are registered through the toolkit operation builder and land in t
- [x] `cpt-insightspec-fr-auth-service-tokens`: A registered service obtains a `sub = service:<name>` JWT verifiable via the same JWKS; an unregistered caller gets 401.
- [ ] `cpt-insightspec-fr-auth-bootstrap`: On an empty persons table with bootstrap enabled, the first IdP login creates a universe admin and emits the audit event; the second login does not.
- [x] `cpt-insightspec-fr-auth-logout`, `cpt-insightspec-fr-auth-csrf`: Local, RP-initiated, and back-channel logout all converge on the same revoke pipeline; state-changing `/auth/*` requests without CSRF token or matching `Origin` are rejected 403.
- [x] `cpt-insightspec-fr-auth-override`: With `override_enabled`, a login carrying `__override=<email>` yields a session whose JWT `sub` and `/auth/me` identity are the target's, with `impersonator_email` naming the caller; an unknown target is denied 403. With the flag at its default the parameter changes nothing.

## 10. Dependencies

Expand Down Expand Up @@ -758,3 +776,4 @@ All endpoints are registered through the toolkit operation builder and land in t
| Bootstrap race on fresh install | First IdP-authenticated colleague wins universe admin | Empty-table-only window; loud audit; off switch; INSTALLER as production path |
| Gateway exchange cache staleness | Revocation reaches the gateway up to `authz_cache_max_age` late | Default 30 s, well inside the 300 s acceptance bound; set 0 for per-request checks |
| Registry misconfiguration | A service gets roles it should not have | Gitops review of every registry change; issuance audit trail |
| `override_enabled` left on in a real environment | Any authenticated user can view the portal as any known person (the email-only Identity lookup spans tenants until #1687) | Default `false` at every layer; Helm value carries a dev/demo-only warning; every use and denial is audited with the real principal |
Loading
Loading