Commit fb5b57c
feat(security): SSO/JWT authentication migration (Phase 1) (#1569)
## Summary
SSO authentication migration — replaces OpenShift OAuth proxy with
direct OIDC authentication via Keycloak. Includes a validated end-to-end
native SSO deployment path (api-server + control plane) that bypasses
the legacy backend and operator entirely.
### What's implemented
- **Frontend BFF OIDC**: Next.js acts as confidential OIDC client.
Browser gets httpOnly session cookie, never sees a raw JWT.
Authorization Code Flow with PKCE. Transparent token refresh (5-min
access tokens, 30-min sessions).
- **Backend JWT validation**: JWKS-based validation via
`lestrrat-go/jwx/v2`. Validates signature, expiration, issuer, and
audience.
- **K8s impersonation**: Backend SA +
`Impersonate-User`/`Impersonate-Group` headers preserve all existing
RBAC enforcement without cluster OIDC federation. Prefers
`preferred_username` over `email` for impersonation (matches Keycloak
identity brokering claim order). Defaults to `system:authenticated`
group when JWT has no groups.
- **Dual-path auth**: JWT validation first, K8s TokenReview fallback for
API keys (SA tokens). Both paths use impersonation.
- **SSO_ENABLED env var**: SSO can be enabled via env var without
depending on Unleash feature flag.
- **API server JWK_CERT_URL**: Production environment no longer
hardcodes the JWKS URL. Configurable via `JWK_CERT_URL` env var or
`--jwk-cert-url` CLI flag, enabling non-RH-SSO OIDC providers (e.g.,
Keycloak).
- **Session expired UX**: Global 401 detection via React Query cache,
blocking dialog with login redirect, no retry storms.
- **E2E test auth**: `client_credentials` grant from Keycloak with K8s
SA fallback.
- **Local Keycloak**: Kind cluster includes Keycloak with pre-configured
realm, dev users, and protocol mappers. `make kind-sso-toggle` switches
between SSO and legacy mode.
- **Native SSO deployment workflow**: Complete workflow for deploying
with Keycloak SSO, validated on hcmais cluster. See
`workflows/deploy-native-sso.md`.
- **Migration ordering fix**: roleBindings `typedFKMigration` ID changed
from `202505130001` to `202603100139` to sort after the table-creation
migration it depends on (`202603100138` creates the `role_bindings`
table; the old ID sorted before it, breaking fresh databases).
**Production DB impact verified**: the `ambient-code` staging RDS
already has `202505130001` in its migrations table. Shipping
`202603100139` means gormigrate treats it as a new migration and runs
it. Every statement uses `IF EXISTS`/`IF NOT EXISTS`/`DROP NOT NULL`
(all idempotent), so all operations no-op on production. The migrations
table will contain both IDs — redundant but harmless. Fresh databases
(Kind, new namespaces) will only have `202603100139` in the correct sort
position.
### Deployment overlays
Overlays reorganized by cluster under
`components/manifests/overlays/hcmais/`:
| Overlay | Purpose |
|---------|---------|
| `hcmais/jsell-sso-poc/` | Native SSO ambient deployment (api-server +
control plane, no oauth-proxy) |
| `hcmais/keycloak/` | Keycloak instance deployment |
### Key files
| Area | Files |
|------|-------|
| Spec | `specs/security/sso-authentication.spec.md` |
| Deployment workflow | `workflows/deploy-native-sso.md` |
| API server JWKS config |
`components/ambient-api-server/cmd/ambient-api-server/environments/e_production.go`
|
| Backend SSO | `components/backend/handlers/sso.go`, `server/server.go`
|
| Frontend OIDC | `components/frontend/src/lib/oidc.ts`, `session.ts`,
`auth.ts` |
| Overlay (SSO PoC) |
`components/manifests/overlays/hcmais/jsell-sso-poc/` |
| Overlay (Keycloak) | `components/manifests/overlays/hcmais/keycloak/`
|
| RBAC | `base/rbac/backend-clusterrole.yaml`,
`base/rbac/control-plane-*` |
### Default behavior
- `make kind-up` deploys with **legacy auth** (SA token, no Keycloak
redirect)
- `make kind-sso-toggle` enables Keycloak OIDC for both frontend and
backend
- Existing deployments using oauth-proxy are unaffected — SSO code paths
only activate with `SSO_ENABLED=true` or the `sso-authentication`
Unleash flag
- API server falls back to `sso.redhat.com` JWKS if `JWK_CERT_URL` is
not set
### Native SSO deployment (validated)
The end-to-end path without legacy components was validated on the
hcmais cluster:
```
acpctl → Keycloak auth → API Server (REST) → DB
↓ gRPC event
Control Plane → Runner Pod → Claude response
↑ gRPC messages
Runner → API Server → acpctl
```
Prerequisites for a new environment:
1. Keycloak with 3 clients: `ambient-frontend`, `ambient-control-plane`
(service account), `ambient-cli` (public)
2. Identity brokering with RH SSO (optional, for RH user federation)
3. Secrets: `sso-credentials`, `control-plane-oidc`, `ambient-vertex`
4. See `workflows/deploy-native-sso.md` for the complete checklist
## Test plan
- [x] Frontend login via Keycloak → session cookie → JWT forwarded to
backend
- [x] Backend validates JWT, impersonates user, RBAC enforced
- [x] API key fallback via TokenReview still works
- [x] Token refresh works silently
- [x] Session expired dialog appears on refresh token expiry
- [x] Logout destroys session + Keycloak single sign-out
- [x] `make kind-sso-toggle` switches between SSO and legacy mode
- [x] Legacy mode (SA token auth) works when SSO is off
- [x] E2E token extraction via Keycloak `client_credentials`
- [x] Native SSO: acpctl login via Keycloak → create project → create
session → runner pod → Claude response
- [x] Native SSO: control plane gRPC watch streams connected to
api-server
- [x] Native SSO: runner authenticates via CP token server,
pushes/watches session messages
- [x] Migration ordering: fresh Kind database bootstraps without errors
- [x] Migration idempotency: existing databases handle re-run safely
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* SSO via OIDC/Keycloak with session-based login/logout, secure cookies,
and backend JWT validation
* Dual-mode auth (legacy & SSO) behind a feature flag for gradual
rollout
* Frontend session-expired dialog, explicit Logged Out page, and
SSO-aware logout flow
* Improved E2E helpers to run tests in both legacy and SSO modes
* **Documentation**
* Updated local dev, Kind/Keycloak setup, E2E, and SSO
migration/deployment guides
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jsell-rh <bot@ambient-code.local>1 parent 351fba7 commit fb5b57c
147 files changed
Lines changed: 5078 additions & 903 deletions
File tree
- .github/workflows
- .specify
- memory
- scripts/bash
- templates
- components
- ambient-api-server
- cmd/ambient-api-server/environments
- pkg/api/openapi
- docs
- plugins/roleBindings
- test/integration
- ambient-cli/cmd/acpctl/project
- backend
- handlers
- jwtauth
- server
- frontend
- src
- app
- api
- auth/sso
- callback
- e2e-login
- login
- logout
- me
- logged-out
- projects/[name]
- sessions/[sessionName]
- sso/[...path]
- components
- providers
- lib
- services/api
- manifests
- base/rbac
- overlays
- hcmais
- jsell-sso-poc
- keycloak
- kind
- runners/ambient-runner
- ambient_runner
- bridges/claude
- platform
- tests
- docs/internal
- deployment
- developer/local-development
- e2e
- cypress/support
- scripts
- specs/security
- workflows
- security
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | | - | |
| 245 | + | |
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
249 | 319 | | |
250 | 320 | | |
251 | 321 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
| 66 | + | |
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
| 150 | + | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
| 156 | + | |
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | | - | |
| 13 | + | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
58 | | - | |
| 57 | + | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
| 86 | + | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | | - | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
| 101 | + | |
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
| 139 | + | |
140 | 140 | | |
141 | 141 | | |
142 | | - | |
| 142 | + | |
143 | 143 | | |
144 | 144 | | |
145 | | - | |
| 145 | + | |
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
223 | | - | |
| 223 | + | |
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
228 | | - | |
| 228 | + | |
229 | 229 | | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | | - | |
21 | | - | |
| 20 | + | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
0 commit comments