feat(auth): expose refresh_expires_in via KeycloakTokens so hook cookie Max-Age tracks refresh window #22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Discovered scope from QA review of PR #20 (closes #15). The hook in PR #20 had to ship with
Max-Age = access_exp(~5 min) because the frozenKeycloakTokensinterface fromforgejo_admin/westside-admin#14(PR #18) doesn't carryrefresh_expires_in. Result: browser discards the session cookie at every access-token expiry — even though the refresh token is still valid for ~30 min — so users hit the login form every ~5 minutes instead of every ~30 minutes.Repo
forgejo_admin/westside-adminUser Story
story-westside-admin-admin-row-crud. Marcus shouldn't be re-logging-in every 5 minutes while he's in the middle of an admin task. The cookie's lifetime should track the refresh window so requests reach the server-side refresh path before the browser drops the session.Context
The Keycloak
/tokenresponse includesrefresh_expires_inalongsideexpires_in. The lib'sRefreshResponseBodyinterface (lines 463-475 ofsrc/lib/server/keycloak.tsin main, post-#18) consumes the field but maps to aKeycloakTokensinterface (lines 476-481) that drops it. The hook in PR #20 readstokens.exp(access exp, derived fromexpires_in) and writes it toMax-Age. The fix:KeycloakTokensto carry bothexp(access) andrefresh_exp(refresh) — derived fromDate.now() + refresh_expires_in*1000at the same momentexpis derived.refreshTokensIfNeededto populate the new field on every refresh.hooks.server.tsto userefresh_expforMax-Age(clamped to a sane upper bound — 24h say — to avoid pathological tokens).File Targets
Update:
src/lib/server/keycloak.ts— extendKeycloakTokensinterface; updaterefreshTokensIfNeededto populaterefresh_exp; update the initial-grant code path (in PR #21's/auth/callbackif not yet merged, or in main if it is) to populate it too.src/lib/server/keycloak.test.ts— add test cases that assertrefresh_expis populated on initial token grant + on refresh.src/hooks.server.ts— replaceMax-Age = exp - nowwithMax-Age = min(refresh_exp - now, 86400)(24h cap).Do NOT touch:
src/routes/auth/*(already merged — they use the lib's surface, will get the new field for free).src/routes/(unauthorized)/*— unrelated.Acceptance Criteria
KeycloakTokenscarriesrefresh_exp: number(epoch seconds).refreshTokensIfNeededpopulatesrefresh_expon every refresh response.refresh_expon the first cookie write.hooks.server.tsreadstokens.refresh_expand writesMax-Age = max(0, min(refresh_exp - now, 86400))to the cookie.npm testexits 0 (existing 25 cases still pass + new cases forrefresh_exppopulation).Test Expectations
npm test):refresh_exp; refresh response populatesrefresh_exp.Constraints
KeycloakTokensbackward-compatible if possible (additive field), but if not, update all consumers in the same PR.refresh_expires_inor any token material.Checklist
feedback_funnel_requires_auth(cookie lifetime is part of the auth surface)Related
project-westside-adminforgejo_admin/westside-admin#14— original lib (the interface this issue extends)forgejo_admin/westside-admin#15— hook that consumes the cookie Max-Age (PR #20)review-1135-2026-05-03-iter-qa— QA review that surfaced this gap (verdict APPROVED-with-followup)feedback_funnel_requires_auth