WIP: feat(session): session policy declared in host code, checked against the Authentik provider #73

Draft
james.maes wants to merge 4 commits from feat/session-policy into develop
pull from: feat/session-policy
Owner

Not automatic logoff by itself. The idle timeout this policy states is enforced by the provider's refresh-token validity, so it counts requests, not user actions: a page that polls or a device that sends heartbeats is not idle. A host that needs automatic logoff after user inactivity (HIPAA 45 CFR 164.312(a)(2)(iii), HITRUST session time-out) also needs QQQ core's user inactivity timeout (QRun-IO/qqq PR 1361); this validity is then the outer bound. GG Forge's confirmed numbers are 15 minutes idle and 12 hours at most (James, 2026-11-10), with a 5 minute access token; the README example now uses them.

What

A host application states in code how long a sign-in lasts, and the qbit checks that the Authentik provider agrees. Nothing is written to Authentik.

  • AuthentikSessionPolicy: idle timeout, absolute maximum and access-token lifetime, in seconds. problems() refuses numbers that cannot be enforced (access token under 120 s, idle timeout under twice the access token, maximum not longer than the idle timeout or over 90 days). applyTo(OAuth2AuthenticationMetaData) sets core's maxSessionLifetimeSeconds and refuses meta-data without refresh tokens or the offline_access scope.
  • AuthentikQBitConfig.withSessionPolicy(...) (needs appClientId): config validation fails when the instance's OAuth2 authentication does not carry the policy.
  • Health check authentik.session_policy (hourly, registered only when a policy is stated): reads the app's provider with GET /providers/oauth2/?client_id= and keeps only the validity settings and grant types. UP when access_token_validity, refresh_token_validity and refresh_token_threshold (0) match; DEGRADED naming each difference in seconds; DOWN when the service account cannot view the provider.
  • The service-account role needs view_oauth2provider as an object permission on the app's own provider, never model-wide (the provider API returns client secrets).

Hosts that state no policy are unaffected: no new check, no new API call, no schema change.

Why this shape

Owner decision (2026-11-10): the timeout is declared in versioned code, not edited afterwards through an admin action. The provider's validities stay in its blueprint; this check is what keeps the two from disagreeing unnoticed.

How the limits are enforced, read in source: QQQ core 4.1.0-RC.29 OAuth2AuthenticationModule renews an expired access token with the stored refresh token and stops at maxSessionLifetimeSeconds; Authentik 2026.8.5 providers/oauth2/views/token.py issues a new refresh token at every renewal when refresh_token_threshold is 0, valid for refresh_token_validity from that moment, and revokes the old one. So the idle timeout is the provider's refresh-token validity.

Verified

  • Gitea build on the branch head 620dfcc (run 7370, mvn -T1C verify, contract stack Authentik 2026.8.3): success. The code is that of 5bf8672, whose build (run 7361) passed with 634 unit tests and 111 contract tests, SessionPolicyContractIT among them; f19ddf8 changes the README only and 620dfcc is an empty commit to re-run CI.
  • Run 7362 (f19ddf8) failed before any contract test ran: the runner could not start Testcontainers (Docker networking error). Unit tests passed in it.
  • The first build (848378d, run 7358) failed in SessionPolicyContractIT: Authentik answers 403, not an empty list, to an account that may view no provider. 5bf8672 reports that as DOWN (unverified) with the grant to add.
  • Local ./mvnw -B verify -DskipITs on 5bf8672: 634 tests, 0 failures, BUILD SUCCESS. Contract ITs were not run locally.
  • Not verified anywhere yet: the health check against a deployed Authentik 2026.8.5.

Release

After merge: a new RC (0.1.0-RC.13) through the normal release flow. GG Forge then pins it and passes its session numbers in (gg-engineering/gg-forge PR 48).

🤖 Generated with Claude Code

> **Not automatic logoff by itself.** The idle timeout this policy states is enforced by the provider's refresh-token validity, so it counts requests, not user actions: a page that polls or a device that sends heartbeats is not idle. A host that needs automatic logoff after user inactivity (HIPAA 45 CFR 164.312(a)(2)(iii), HITRUST session time-out) also needs QQQ core's user inactivity timeout (QRun-IO/qqq PR 1361); this validity is then the outer bound. GG Forge's confirmed numbers are 15 minutes idle and 12 hours at most (James, 2026-11-10), with a 5 minute access token; the README example now uses them. ## What A host application states in code how long a sign-in lasts, and the qbit checks that the Authentik provider agrees. Nothing is written to Authentik. - `AuthentikSessionPolicy`: idle timeout, absolute maximum and access-token lifetime, in seconds. `problems()` refuses numbers that cannot be enforced (access token under 120 s, idle timeout under twice the access token, maximum not longer than the idle timeout or over 90 days). `applyTo(OAuth2AuthenticationMetaData)` sets core's `maxSessionLifetimeSeconds` and refuses meta-data without refresh tokens or the `offline_access` scope. - `AuthentikQBitConfig.withSessionPolicy(...)` (needs `appClientId`): config validation fails when the instance's OAuth2 authentication does not carry the policy. - Health check `authentik.session_policy` (hourly, registered only when a policy is stated): reads the app's provider with `GET /providers/oauth2/?client_id=` and keeps only the validity settings and grant types. UP when `access_token_validity`, `refresh_token_validity` and `refresh_token_threshold` (0) match; DEGRADED naming each difference in seconds; DOWN when the service account cannot view the provider. - The service-account role needs `view_oauth2provider` as an object permission on the app's own provider, never model-wide (the provider API returns client secrets). Hosts that state no policy are unaffected: no new check, no new API call, no schema change. ## Why this shape Owner decision (2026-11-10): the timeout is declared in versioned code, not edited afterwards through an admin action. The provider's validities stay in its blueprint; this check is what keeps the two from disagreeing unnoticed. How the limits are enforced, read in source: QQQ core 4.1.0-RC.29 `OAuth2AuthenticationModule` renews an expired access token with the stored refresh token and stops at `maxSessionLifetimeSeconds`; Authentik 2026.8.5 `providers/oauth2/views/token.py` issues a new refresh token at every renewal when `refresh_token_threshold` is 0, valid for `refresh_token_validity` from that moment, and revokes the old one. So the idle timeout is the provider's refresh-token validity. ## Verified - Gitea build on the branch head 620dfcc (run 7370, `mvn -T1C verify`, contract stack Authentik 2026.8.3): success. The code is that of 5bf8672, whose build (run 7361) passed with 634 unit tests and 111 contract tests, `SessionPolicyContractIT` among them; f19ddf8 changes the README only and 620dfcc is an empty commit to re-run CI. - Run 7362 (f19ddf8) failed before any contract test ran: the runner could not start Testcontainers (Docker networking error). Unit tests passed in it. - The first build (848378d, run 7358) failed in `SessionPolicyContractIT`: Authentik answers 403, not an empty list, to an account that may view no provider. 5bf8672 reports that as DOWN (unverified) with the grant to add. - Local `./mvnw -B verify -DskipITs` on 5bf8672: 634 tests, 0 failures, BUILD SUCCESS. Contract ITs were not run locally. - Not verified anywhere yet: the health check against a deployed Authentik 2026.8.5. ## Release After merge: a new RC (0.1.0-RC.13) through the normal release flow. GG Forge then pins it and passes its session numbers in (gg-engineering/gg-forge PR 48). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 17:37:19 +00:00
feat(session): session policy declared in host code, checked against the Authentik provider
maven / build (push) Failing after 11m58s
build / maven (push) Failing after 11m58s
848378d3bc
AuthentikSessionPolicy states an idle timeout, an absolute maximum and the
access-token lifetime. The config refuses numbers that cannot be enforced and
an instance whose OAuth2 authentication does not carry them. The new
authentik.session_policy health check reads the app's OAuth2 provider (validity
settings and grant types only) and reports DEGRADED when it differs, DOWN when
the service account cannot view it. Nothing is written to Authentik.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes added 1 commit 2026-11-10 17:40:05 +00:00
fix(session): Authentik answers 403 when the account may view no provider
maven / build (push) Successful in 11m39s
build / maven (push) Successful in 11m39s
5bf86725eb
Seen in the contract build on Authentik 2026.8.3: GET /providers/oauth2/ is
refused (403), not empty, for a service account with no provider permission.
The health check reports that as DOWN (unverified) with the grant to add, and
the contract test expects the refusal before and after the temporary grant.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes added 1 commit 2026-11-10 18:13:20 +00:00
docs(session): the idle timeout counts requests; example at 15 minutes
maven / build (push) Failing after 1m33s
build / maven (push) Failing after 1m33s
f19ddf83ee
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes added 1 commit 2026-11-10 18:43:42 +00:00
ci: re-run (run 7362 could not start Testcontainers: runner Docker networking error; unit tests passed)
maven / build (push) Successful in 12m30s
build / maven (push) Successful in 12m30s
620dfccb7f
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
All checks were successful
maven / build (push) Successful in 12m30s
build / maven (push) Successful in 12m30s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/session-policy:feat/session-policy
git checkout feat/session-policy
Sign in to join this conversation.