feat: add OIDC browser sessions with live access enforcement
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
tegwick 2026-09-28 12:17:16 +02:00
parent fdea2f5192
commit a15fe032b0
14 changed files with 807 additions and 31 deletions

View file

@ -12,7 +12,7 @@ upgrade:** the default production factory has no admitted owner adapters yet and
returns 401 for missing credentials and 503 for credential-bearing requests.
The current deployed image and its release configuration have not been changed.
Only exact `GET /healthz` is public, returning `{"status":"ok"}`. The shared
Without explicit browser composition, only exact `GET /healthz` is public, returning `{"status":"ok"}`. The shared
ASGI boundary protects docs, readiness, native ports, projections, compatibility
aliases and subsequently attached routes. Unknown method/route/handler combinations,
WebSockets, mounts without admission, and slash redirects without catalog entries
@ -26,6 +26,10 @@ It is packaged in the wheel and tested against actual route construction. Source
inventory generation does **not** auto-admit a new route. Duplicate docs handlers
remain separately inventoried; the boundary selects the first effective route.
These technical action names require flex-auth/owner review before policy delivery.
Optional browser composition adds four exact `/auth/*` protocol routes and the
`hub.browser.session` action; login initiation is public, callback state is
browser-bound, and session access requires current root authority. See the
[browser integration contract](owner-access-integration.md#browser-composition-source-candidate).
## Composition and trust
@ -76,7 +80,7 @@ and Phase 2 storage/query isolation are not implemented by this classification.
The PDP request carries actor, target tenant, action, concrete resource path,
assurance, root entitlement, authoritative evidence reference, and a digest of
HTTP method/path/query/body. Client bodies and bearer tokens are not sent to the
PDP or audit sink. Body size is bounded at 1 MiB. The controller's identity/facts/
PDP or audit sink. Body size is bounded at 1 MiB with a ten-second receive deadline. The controller's identity/facts/
policy/audit chain has a ten-second timeout; individual HTTP calls have three seconds.
A separate refusal-audit attempt is bounded at three seconds.

View file

@ -0,0 +1,43 @@
# HUB-WP-0012 browser source evidence — 2026-09-28
This is local implementation evidence, not live identity, entitlement, policy,
archive-custody or deployment acceptance.
Implemented explicit browser composition with confidential OIDC code exchange,
S256 PKCE, one-time browser-bound state, signed ID-token validation, nonce,
authentication freshness, optional access-token hash binding and live root
access authorization before session creation. Secure host-only cookies contain
opaque IDs; tokens remain in bounded process-local memory. Sessions last no
longer than five minutes or either token, and restart invalidates them.
Cookie writes require exact Origin and CSRF; every protected request still
rechecks identity/facts/policy/audit. Session validity is rechecked after authority
awaits. Local logout remains available during owner outages. Browser refusal
records exclude authorization codes, tokens and query parameters.
Two additional regressions are fixed: explicit controllers cannot silently run
without enforcement, and slow request bodies time out before authority/handlers.
Validation:
- `HUB_CORE_AUDIT_CORE_SOURCE=/home/worsch/audit-core .venv/bin/python -m pytest -q --disable-warnings`:
**328 passed**, one existing Starlette/httpx deprecation warning.
- After the final correlation-ID adjustment, the browser suite passed again:
**22 passed**. It uses actual RSA signatures, mock OIDC discovery/code exchange
and synthetic authority/policy/audit owners. It covers replay/browser binding,
bad nonce/audience/subject/authorized party/type/time/hash, non-root/non-platform/
non-MFA denial, expiry/restart, grant withdrawal, CSRF and mixed credentials,
logout during authority awaits, capacity and owner outage denial. A real native
message handler accepts a valid session write and refuses a spoofed sender.
- Wheel/source distribution build passes; the browser module is packaged.
- Inventory drift/coverage check passes: **165 Hub source surfaces**, 48 platform
rows and 250 dated cluster objects. Four optional browser protocol routes have
their own profile; inventory coverage is not live conformance.
- `git diff --check` passes.
Remaining gates: confidential issuer registration and real MFA behavior, immutable
root and authoritative owner-facts integration, Hub policy admission including
`hub.browser.session`, durable production audit delivery, proxy logging/rate limits,
consumer adoption and complete platform acceptance. The source candidate provides
neither distributed sessions, upstream IdP logout, automatic renewal nor operation
completion auditing. T01–T04 remain in progress. No public listener or production
entitlement changed.

View file

@ -99,3 +99,56 @@ A composed fixture journey verifies a real RSA JWT, fresh facts, an Ed25519
policy decision, receiver custody and native Hub write, followed by entitlement
withdrawal denial. Real root enrollment/login, service deployment, live key and
sender custody, and operational acceptance remain open.
## Browser composition (source candidate)
Pass `browser_settings=BrowserSettings(origin="https://hub.example",
client_id="hub-browser", client_secret_file=Path("/run/secrets/hub-oidc-client"))`
to `create_app`, alongside the enforced controller or authoritative-facts security
composition described above. Import `BrowserSettings` from
`hub_core.security.browser`. Browser configuration cannot activate a development
runtime or a missing controller. Its confidential-client credential is separate
from the policy/audit workload credentials and is reread at each code exchange.
Register the exact HTTPS origin plus `/auth/callback` with the admitted issuer.
The source candidate requires authorization code, S256 PKCE, RS256 ID tokens and
`client_secret_basic` discovery support. It requests fresh authentication and
AAL2; validated access-token assurance and current owner facts decide access.
Issuer registration, actual MFA behavior, audience mapping and live owner
acceptance remain deployment gates. The implementation follows the
[OIDC ID token validation contract](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)
and [OAuth security best practices](https://www.rfc-editor.org/rfc/rfc9700.html).
- `GET /auth/login` initiates login; arbitrary query parameters are refused.
- `GET /auth/callback` consumes browser-bound state once, exchanges the code with
PKCE, checks signature/issuer/audience/nonce/authentication time/access binding,
and requires root authorization and audit custody before issuing a session.
- `GET /auth/session` rechecks authority and returns expiry and a CSRF token.
- `POST /auth/logout` requires the session, exact `Origin` and `X-Hub-CSRF`, then
destroys the local session even during identity/policy/audit outages.
Sessions contain access tokens only in server memory. Cookies carry random opaque
IDs with `Secure`, `HttpOnly`, `SameSite=Lax`, host-only scope and `/` path. Sessions
expire within five minutes or either token's expiry, whichever comes first;
there is no refresh-token renewal. Process restart/shutdown invalidates sessions.
Multiple workers need sticky routing or a separately reviewed shared session
store; this candidate does not provide distributed sessions. Pending logins and
sessions have bounded capacity. Production ingress must also rate-limit login.
All protected API requests recheck identity, current facts, signed policy and
durable audit. Cookie-authenticated writes additionally require exact `Origin`
and `X-Hub-CSRF`; browser clients obtain the latter from `/auth/session`. The
bundled Swagger UI does not automatically inject that header. Mixed cookies and
bearer credentials are rejected. Session expiry/logout is checked again after
authority awaits and before dispatch. Logout cannot cancel already executing
requests or log out the upstream identity provider; subsequent login requests
fresh authentication. Logout is local invalidation, not a durable audited
business mutation. Refused browser flows are recorded without codes/tokens/query
parameters; audit failure returns 503. Login initiation does not require authority.
The app must observe the configured HTTPS origin. Do not trust arbitrary
forwarded headers; configure an authenticated trusted proxy separately. Callback
query strings are cleared before response-time application access logging, but
reverse proxies and tracing collectors must independently suppress callback
queries, cookies and authorization headers. No public listener is enabled by
these source changes.

View file

@ -118,6 +118,23 @@
"audience": "none: minimal process liveness",
"test_owner": "hub-core",
"enforcement": "No identity required; no sensitive details"
},
"browser-session": {
"actor_tenant": "tenant:platform for root; named workload tenant otherwise",
"target_tenant": "resolved server-side from resource; root cross-tenant action explicitly audited",
"action_resource_rule": "hub.browser.session for login/session authority; protected API uses existing route action",
"cases": [
"ROOT",
"OTHER",
"INVALID",
"REVOKE",
"OUTAGE",
"BYPASS",
"CALLER"
],
"audience": "hub-browser (OIDC ID token); hub-core (access token); deployment registration required",
"test_owner": "hub-core",
"enforcement": "Explicit BrowserSessions: login is public initiation; callback requires one-time browser-bound state, PKCE and nonce; session requires current authority; logout requires session and CSRF"
}
},
"acceptance_cases": {
@ -399,6 +416,39 @@
"conditional": false,
"handler": "list_widgets"
},
{
"id": "http:GET:/auth/callback:hub_core.security.browser.browser_flow",
"kind": "runtime-http",
"method": "GET",
"path": "/auth/callback",
"profile": "browser-session",
"current_gate": "OIDC state/PKCE callback or server-side session; explicit browser composition only",
"source": "hub_core.security.browser",
"conditional": true,
"handler": "browser_flow"
},
{
"id": "http:GET:/auth/login:hub_core.security.browser.browser_flow",
"kind": "runtime-http",
"method": "GET",
"path": "/auth/login",
"profile": "browser-session",
"current_gate": "OIDC state/PKCE callback or server-side session; explicit browser composition only",
"source": "hub_core.security.browser",
"conditional": true,
"handler": "browser_flow"
},
{
"id": "http:GET:/auth/session:hub_core.security.browser.browser_flow",
"kind": "runtime-http",
"method": "GET",
"path": "/auth/session",
"profile": "browser-session",
"current_gate": "OIDC state/PKCE callback or server-side session; explicit browser composition only",
"source": "hub_core.security.browser",
"conditional": true,
"handler": "browser_flow"
},
{
"id": "http:GET:/console:hub_core.runtime.compat.console",
"kind": "runtime-http",
@ -982,6 +1032,17 @@
"conditional": false,
"handler": "create_widget"
},
{
"id": "http:POST:/auth/logout:hub_core.security.browser.browser_flow",
"kind": "runtime-http",
"method": "POST",
"path": "/auth/logout",
"profile": "browser-session",
"current_gate": "OIDC state/PKCE callback or server-side session; explicit browser composition only",
"source": "hub_core.security.browser",
"conditional": true,
"handler": "browser_flow"
},
{
"id": "http:POST:/decision-records:hub_core.runtime.compat.accept_deferred",
"kind": "runtime-http",

View file

@ -1,7 +1,7 @@
# Platform-root access inventory — 2026-09-28
This is the coverage baseline for HUB-WP-0012-T01, not an access grant or a
passing security test. It enumerates 161 Hub source surfaces (88 runtime route
passing security test. It enumerates 165 Hub source surfaces (92 runtime route
registrations, 42 embedded router operations, 31 MCP tools), 39 observed cluster
namespaces and nine additional extension/native-management boundaries. All 250
observed Deployment/StatefulSet/DaemonSet/CronJob/Service/Ingress objects map to
@ -19,7 +19,9 @@ pending owner confirmation; none establishes an effective platform-root grant.
Hub source revision is recorded in the JSON. Runtime routes are constructed
locally without running startup, sending requests or connecting to a database.
The optional inbox router is included separately. Compatibility aliases and
The optional inbox and browser-session routers are included separately. Browser
login initiation and callback are exact protocol entry points; they do not
grant access without verified tokens and live root authorization. Compatibility aliases and
FastAPI built-in documentation endpoints are included even when absent from
OpenAPI; disabled compatibility groups still belong in the coverage contract.
Embedded factories are scanned with their default prefixes and include optional