diff --git a/scripts/authentik-set-login-session-duration.sh b/scripts/authentik-set-login-session-duration.sh new file mode 100755 index 0000000..a4048fb --- /dev/null +++ b/scripts/authentik-set-login-session-duration.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Set the authentik login-session TTL on Dankclaw. +# +# The login-session lifetime is governed by the `default-authentication-login` +# stage's `session_duration`, NOT a global setting and NOT SESSION_COOKIE_AGE +# (that is the pre-login session, sessions.unauthenticated_age). Authentik does +# not expose SESSION_SAVE_EVERY_REQUEST at all, so the session is an absolute +# window from login, not a sliding one. +# +# The field is a STRING in authentik's own idiom ("days=7", "seconds=30", +# "hours=12") — not a Python timedelta. Writing str(timedelta(days=7)) stores +# "7 days, 0:00:00", which authentik's timedelta_from_string parser REJECTS +# (ValueError), and login then crashes computing the session expiry. Always pass +# the "key=value" form; this script validates it before writing. +# +# Existing sessions keep their old expiry; the new TTL applies from the next +# login. This is DB state — it is NOT repo-managed and a blueprint re-import or +# rebuild-from-scratch reverts it. Run this again after such a rebuild. +# +# Usage: authentik-set-login-session-duration.sh ["days=7"] (default days=7) +set -euo pipefail + +DUR="${1:-days=7}" +STAGE="default-authentication-login" +HOST="${AUTHENTIK_HOST:-dankclaw}" + +# Refuse anything that is not the safe key=value duration idiom. +if ! printf '%s' "$DUR" | grep -Eq '^[A-Za-z]+=[0-9]+([ ]+[A-Za-z]+=[0-9]+)*$'; then + echo "invalid duration '$DUR' — use authentik form, e.g. 'days=7' or 'seconds=30'" >&2 + exit 2 +fi + +ssh "$HOST" 'podman exec -i authentik-server ak shell' <", timedelta_from_string(back)) +PYEOF + +echo "Login-session TTL for '$STAGE' is now $DUR (applies on next login)." diff --git a/serverAppConfigs-docs/Infrastructure/Authentik.md b/serverAppConfigs-docs/Infrastructure/Authentik.md index d43def8..5dea029 100644 --- a/serverAppConfigs-docs/Infrastructure/Authentik.md +++ b/serverAppConfigs-docs/Infrastructure/Authentik.md @@ -267,6 +267,12 @@ The silent re-auth path was proven end-to-end: after deleting `qa-e2e-admin`'s rows, the e2e admin spec ran against the stale storage state and passed with no login prompt (2 tests, 2.9 s). +## Login-session lifetime (how often you re-authenticate) + +How long a login stays valid is the **`default-authentication-login` stage's `session_duration`** (`UserLoginStage`) — not a global setting, and not `SESSION_COOKIE_AGE` (that is the *pre-login* session). Raised to **`days=7`** on 2026-10-20 (was stage value `0` = the 24 h framework default). It is an **absolute** window, never sliding: authentik implements `SESSION_SAVE_EVERY_REQUEST` nowhere, so activity does not extend it and no supported knob makes it one. + +**`session_duration` is a STRING** in authentik's idiom (`"days=7"`, `"seconds=30"`, `"hours=12"`). A Python `timedelta` stores `"7 days, 0:00:00"`, which `authentik.lib.utils.time.timedelta_from_string` rejects with `ValueError` and login then crashes computing the expiry — so set it with **`scripts/authentik-set-login-session-duration.sh`** (validates the form first). Any non-zero value applies to *every* login (the "remember me" box only adds `remember_me_offset` on top) and a positive `set_expiry` also overrides `SESSION_EXPIRE_AT_BROWSER_CLOSE`, so closing the browser no longer logs you out; **existing sessions keep their old expiry** — delete the user's `authentik_core_session`/`…proxysession` rows and re-login to force it now. This is **DB state, not repo-managed**: a blueprint re-import or rebuild reverts the stage to `0`, so re-run the script after one. The other two login stages (`default-source-*`) drive external-source/enrollment logins and stay at the framework default. + ## Guard-chain flip recipe (adding a guarded vhost) Move a public vhost from `reverse_proxy :` onto the chain: