Auth: silently refresh expired sessions and return users to the page they were on #6
Problem
The SSO (kanidm) issues short-lived tokens, so the login session expires quickly. Two user-visible pains:
- Lost location. After re-authenticating, the user lands on the dashboard instead of the page they were on.
- Before:
https://gitshark.ha1nz.de/repos/workaround/Gitshark/issues - After:
https://gitshark.ha1nz.de/
- Before:
- Visible re-auth. Every ID-token expiry forces a round-trip through the IdP (or a manual "Log in" click). This should be invisible.
Research / current state
quarkus.oidc.application-type=web-app;quarkus.oidc.token.refresh-expiredis not set (defaultfalse). When the ID token expires, Quarkus invalidates the session cookie instead of using the refresh token. Defaultquarkus.oidc.authentication.session-age-extensionis only 5 minutes.- Most pages are anonymous-permitted (
quarkus.http.auth.permission.authenticated.pathsonly protects/login,/settings/*,/repos/new,/onboarding,/following*). So after expiry the user silently becomes logged out on public pages, clicks Log in (layout.html:28→/login), andHomeResource.login()hardcodesResponse.seeOther("/")(HomeResource.java:97-104) — that is where the original page is lost. - The UI is fully server-rendered (no htmx/fetch/XHR in any template), so a server-side silent refresh is sufficient; no JavaScript refresh machinery and no
java-script-auto-redirecthandling needed. - kanidm facts:
- Access/ID tokens are short-lived by design (~15 min in the refresh-token design doc); extending them meaningfully on the SSO side is not the intended path.
- OAuth2 refresh token expiry is currently hard-coded to 16 h (kanidm#3040 tracks making it configurable;
offline_accessis discussed in kanidm#4034). So the practical ceiling for a fully silent session is ~16 h; beyond that the code flow runs again (still silent while the kanidm SSO session is alive). - No
end_session_endpoint(already documented inapplication.properties), irrelevant here. - kanidm validates
redirect_uristrictly, which matters for Part A below.
Proposal
Part A — return to the original page after login
- Point the header Log in link at
/login?redirect=<current path + query>(URL-encoded), and makeHomeResource.login()redirect there instead of the hardcoded/.- Open-redirect guard: accept only local paths — must start with
/, must not start with//or contain a scheme; otherwise fall back to/.
- Open-redirect guard: accept only local paths — must start with
- For challenges triggered on protected paths (e.g. deep link to
/settings/...while logged out), rely on Quarkus restoring the original request URI. If a fixed callback is registered in kanidm (strictredirect_urimatching), set:
Withquarkus.oidc.authentication.redirect-path=/login quarkus.oidc.authentication.restore-path-after-redirect=truerestore-path-after-redirect=trueQuarkus sends the user back to the originally requested URL after the code flow; only direct visits to/loginhit the redirect-param logic.
Part B — silent (invisible) session refresh
quarkus.oidc.token.refresh-expired=true
# keep the session cookie alive past ID-token expiry so the refresh can run;
# align with kanidm's 16 h refresh-token lifetime
quarkus.oidc.authentication.session-age-extension=PT12H
# refresh proactively shortly before expiry instead of on the first failing request
quarkus.oidc.token.refresh-token-time-skew=PT60S
- The default token-state-manager strategy (
keep-all-tokens) already stores the refresh token in the encrypted session cookie, andtoken-state-manager.encryption-secretis already set for multi-pod deployments — no change needed there. - Because refresh happens inline during request processing (no redirect), it is invisible to the user and form POSTs survive an expired-but-refreshable token.
- If the refresh itself fails (refresh token older than 16 h / revoked), Quarkus falls back to the code-flow redirect; while the kanidm SSO session is still valid that is also silent, otherwise the user sees the login screen once and Part A returns them to their page.
- Verify kanidm actually issues a refresh token to this client (scopes/basic client config); if not, that is the first fix.
Part C — SSO side (kanidm)
- Extending access-token lifetime in kanidm is not really available/intended; the refresh-token path above is the supported mechanism. Document the ~16 h silent-session ceiling and revisit when kanidm#3040 lands.
Acceptance criteria
- [ ] Session expiring while browsing does not visibly log the user out; the next request refreshes tokens silently (test with a short-lived dev token)
- [ ] After a full re-login, the user lands on the exact page (path + query) they were on
- [ ]
redirectparameter on/loginrejects external/absolute URLs (open-redirect test) - [ ] Form POST submitted with an expired-but-refreshable session succeeds without data loss
- [ ] Failing tests first, green after implementation (repo convention)
- [ ] Docs updated in the same PR:
docs/admins/getting-started.md(new OIDC properties incl. any new env vars),docs/users/if the login behavior description changes
References
- Quarkus OIDC code-flow session management (
refresh-expired,session-age-extension): https://quarkus.io/guides/security-oidc-code-flow-authentication restore-path-after-redirect/redirect-path: https://quarkus.io/guides/security-oidc-expanded-configuration- kanidm refresh-token design (short access tokens, 16 h refresh expiry): https://kanidm.github.io/kanidm/stable/developers/designs/oauth2_refresh_tokens.html
- kanidm configurable refresh expiry: https://github.com/kanidm/kanidm/issues/3040
- kanidm offline_access: https://github.com/kanidm/kanidm/issues/4034