QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 38s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 38s
qbit release candidate / cut-rc (pull_request) Failing after 18s
Release candidate / release-rc (pull_request) Failing after 18s
qbit release candidate / tag-and-release (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Failing after 15m39s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Failing after 24s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Failing after 1h14m34s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 10s
Shared Java Maven CI with Docker / verify (push) Failing after 17m19s
Shared Java Maven CI with Docker / gate (push) Failing after 24s
qrun-release / release (push) Failing after 1h16m30s
qrun-release / postpublish (push) Skipped
Add user and group QQQ application-role access editing, bounded catalogs, webhook inbox and SQL mirrors, refreshed session permissions, and deferred invitation assignments. Include the isolated local review host, browser acceptance scripts, documentation and the token-widget column mapping fix. User approved this RC checkpoint. Full 1.0 gates remain documented, including the schema-drift matcher and PostgreSQL integration errors from the full gate. Companion UI: QRun-IO/qqq-frontend-next#205. Co-Authored-By: GPT-6 <noreply@openai.com>
148 lines
9.0 KiB
Markdown
148 lines
9.0 KiB
Markdown
# Immutable identity for session reevaluation
|
|
|
|
The request-time authentication adapter remains opt-in and is not yet ready for
|
|
production enablement. `AuthentikRefreshingOAuth2Module` connects core OAuth
|
|
authentication to durable bindings and the refreshing mapper. H2/PostgreSQL
|
|
tests exercise native signature verification, cookie/refresh exchange, logout
|
|
and old-token rejection after retention cleanup. A real Authentik-issued-token
|
|
contract verifies current-grant revocation through the adapter. Separate module
|
|
instances share SQL state; independently deployed nodes and interactive browser
|
|
acceptance remain release gates.
|
|
|
|
Mirror authorization expires fifteen minutes after the last published scan's
|
|
start time. Incremental reconciliation is scheduled every five minutes so the
|
|
next scan can finish before that expiry. Busy ticks do not extend freshness;
|
|
targeted webhook updates also do not make an old directory baseline fresh.
|
|
If synchronization stalls beyond the limit, authentication fails closed. Size
|
|
the host and API budget so scans finish within this window; realistic load
|
|
verification remains required before production enablement.
|
|
|
|
## Directory API outages
|
|
|
|
The refreshing adapter reads committed mirror permissions on every request;
|
|
it does not retry the directory API in the request path. This provides the
|
|
mapper's bounded outage fallback without merging old token groups, policies or
|
|
session permissions. H2 and PostgreSQL acceptance tests use native signed OAuth
|
|
verification, force reconciliation API reads to fail, and verify that the failed
|
|
scan cannot advance the watermark. Fresh mirrored access remains usable; committed
|
|
revocations, inactive identities and snapshots older than fifteen minutes do not.
|
|
|
|
This applies to `AuthentikRefreshingOAuth2Module` with
|
|
`AuthentikRefreshingPermissionMapper`, after immutable identity mapping and durable
|
|
binding setup. The legacy `AuthentikGroupPermissionMapper` is the compatibility
|
|
sign-in customizer and does not provide request-time session reevaluation. Hosts
|
|
must migrate to the refreshing pair to obtain these guarantees. OAuth signature,
|
|
issuer, audience, lifetime and binding checks still run: directory API fallback
|
|
does not bypass an unavailable OAuth login or refresh endpoint. Configured cut-over
|
|
guards retain their existing fail-safe host-permission behavior if their live API
|
|
comparison cannot run; the outage acceptance test covers an already enforced
|
|
mapper without cut-over guard groups. Widget outage behavior and deployed-host
|
|
acceptance are separate release gates.
|
|
|
|
The live `TokenContractIT.existingOAuthTokenReflectsReconciledPermissionRevocation`
|
|
now also passes with an Authentik-issued token and real provider discovery/keys.
|
|
It removes a real group permission, publishes the directory through the production
|
|
worker into H2, and proves that the same token loses access through a second
|
|
module instance. Durable logout prevents reuse. Interactive login/token refresh
|
|
and independently deployed application nodes remain separate acceptance gates.
|
|
|
|
The native authorization-code exchange, stored-cookie resume and refresh-token
|
|
HTTP exchange now also pass against the loopback OAuth fixture on H2 and
|
|
PostgreSQL. The tests use core session/refresh-token metadata and encrypted
|
|
refresh-token storage. Refresh replaces the stored access token without extending
|
|
the identity binding deadline; current grants apply, missing bindings reject,
|
|
and logout removes both core credential rows. This is protocol acceptance,
|
|
not a browser login against the actual Authentik authorization flow.
|
|
|
|
The kof22 provider uses `sub_mode: user_email`. Email and username can change
|
|
or be reused, so neither is sufficient to bind an existing QQQ session to an
|
|
Authentik user. Keep the existing subject mode for host compatibility and add
|
|
the managed **QQQ - immutable user identity** profile scope mapping from
|
|
[the tested infrastructure blueprint](../src/test/resources/blueprints/kof22-oauth2-apps.yaml).
|
|
Attach it to the application's OAuth provider alongside its existing property
|
|
mappings; do not replace the provider's other mappings.
|
|
|
|
The mapping returns:
|
|
|
|
```python
|
|
if not request.user.is_authenticated:
|
|
return {}
|
|
return {"qqq_identity": {"pk": request.user.pk, "uuid": str(request.user.uuid)}}
|
|
```
|
|
|
|
The app must request `profile`. A token without that scope has no identity
|
|
claim and cannot be used by the forthcoming reevaluation adapter. This is an
|
|
additional signed claim, not a change to the OAuth subject or an authorization
|
|
grant. Existing authentication behavior remains unchanged until the adapter is
|
|
explicitly configured.
|
|
|
|
`AuthentikSessionIdentity.fromVerifiedClaims` accepts only a positive integral
|
|
pk within the API's integer range and a canonical UUID. It does not verify JWT
|
|
signatures, issuer, audience or lifetime. It must receive claims from the
|
|
authenticated OAuth path, never from request parameters or a decoded but
|
|
unverified token. Missing or malformed claims have no email/username fallback.
|
|
|
|
The adapter must bind a newly verified login to that pk and UUID using
|
|
`AuthentikSessionBindings`. Cookie-session resumption must load the existing
|
|
binding; it must never recreate a missing, expired or revoked binding from old
|
|
claims. The original login deadline is fixed. An existing core user-reference
|
|
change must not reassign the binding to a different identity.
|
|
|
|
On reevaluation, `AuthentikMirrorAccess` supplies only fresh, committed directory
|
|
grants for the bound identity. Its result must replace stale directory/token
|
|
grants while preserving separately established host contributions and credential
|
|
restrictions. A denied or unavailable read cannot justify restoring old claims.
|
|
This replacement and the cross-node request path remain release acceptance gates.
|
|
|
|
Evidence: `TokenContractIT.clientCredentialsClaimsRequireProfile` mints real
|
|
Authentik tokens, compares the claim with the API user record, checks its absence
|
|
without `profile`, then changes both username and email and verifies that the
|
|
claim is unchanged while `sub` changes. Interactive authorization-code/session
|
|
refresh acceptance remains separate. The production provider mapping has not
|
|
been deployed; coordinate it with the host adapter before feature enablement.
|
|
|
|
## Adapter configuration and behavior
|
|
|
|
The opt-in host module code reference is `AuthentikRefreshingOAuth2Module`,
|
|
with `AuthentikRefreshingPermissionMapper` (or a host subclass) as the OAuth
|
|
customizer. Configure `sessionStoreEnabled=false` and a positive
|
|
`maxSessionLifetimeSeconds` no greater than 2,592,000 (thirty days). The adapter rejects missing prerequisites rather
|
|
than changing shared metadata during requests. Core bearer cache hits bypass
|
|
the customizer, so leaving that cache enabled would defeat reevaluation.
|
|
|
|
The host must install the local sync/binding schema and publish a fresh mirror
|
|
before enabling this module. Verified authorization-code logins bind immutable
|
|
identity and original core user reference; cookie resumption only looks up an
|
|
existing binding. Missing, expired, revoked or reassigned bindings reject.
|
|
|
|
Set `authentikPublicUrl` to the public root of the same Authentik installation
|
|
used by `baseUrl` for API reads. It defaults to `baseUrl`; set it explicitly when
|
|
the API uses an internal service address. On login and every permission refresh,
|
|
the mapper checks the verified `iss` origin (scheme, host and effective port)
|
|
and deployment-path boundary against that public root before reading the mirror.
|
|
Native OAuth still verifies the signature and exact provider/audience allowlist;
|
|
this additional check prevents an independently trusted issuer from borrowing
|
|
matching local user IDs. Both global and per-provider issuer paths within the
|
|
installation are supported. Userinfo, query strings, fragments, escaped paths
|
|
and dot-segment/duplicate-slash ambiguity fail closed. Use canonical HTTP(S) URLs.
|
|
|
|
Verified bearer and service credentials use the same durable binding. Their
|
|
absolute deadline is the earlier of token `exp` and signed `iat` plus the
|
|
configured session lifetime. The adapter requires an integral, positive `iat`
|
|
that is not in the future; keep the provider and host clocks synchronized.
|
|
It enforces this deadline on every bind and resume, even if core's optional
|
|
bearer-token lifetime limit is disabled. Cookie login deadlines remain fixed
|
|
at the verified callback, and cookie resumption never recreates missing bindings.
|
|
|
|
Repeated binding cannot extend the original deadline. Retention keeps logout
|
|
records for thirty days after binding expiry; the lifetime cap and signed-age
|
|
check prevent a still-signed old bearer from creating a new binding after those
|
|
records are purged. Logout records durable revocation before core
|
|
credential cleanup. Every successful resume and validity check refreshes
|
|
permissions, applying credential restrictions after current grants.
|
|
|
|
Host permissions must be supplied by current static-group configuration or the
|
|
host permission source during blocked cut-over. Do not restore an old session's
|
|
permission set in a host customizer. Interactive OAuth, token refresh, real
|
|
same-token revocation across nodes and provider rollout remain acceptance gates.
|