fix(auth): bound session lifetime and make OIDC token refresh resilient - #8
Merged
Conversation
… token refresh logic Signed-off-by: Ntege Daniel <danientege785@gmail.com>
… token refresh logic Signed-off-by: Ntege Daniel <danientege785@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
createAppLayerAuth()had three defects that cost consuming apps (e.g.open-tasks) theirsession after roughly one access-token lifetime, and hammered the IdP token endpoint when that
lifetime was short. All three are fixed in the library — consumers need no workaround.
Public API is unchanged:
createAppLayerAuth()still returns{ handlers, auth, signIn, signOut, oidcIssuer }, and calling it with only{ issuer, clientId, clientSecret }keeps working. Released as 0.7.1.Defects fixed
session: { strategy: "jwt" }inherited the Auth.js default of30 days, so the session cookie outlived the access token by weeks: the middleware said
"authenticated" while every proxied API call returned 401.
(a common Authentik default), the refresh window was open from the moment the token was
minted — a refresh was attempted on every RSC render, session poll, and proxied call.
a timeout) permanently set
RefreshTokenError;authorized()ignored it and admitted theuser into a dead app shell; concurrent requests each POSTed the same refresh token, so
rotation-enabled IdPs invalidated all but one (random logouts); discovery was re-fetched on
every refresh; neither fetch had a timeout.
Changes
New
src/server/oidc-tokens.ts— all token logic, split out ofauth.tsso it isunit-testable without a live IdP:
max(5, min(skew, floor(lifetime / 2))),with
tokenLifetimepersisted on the JWT (fromaccount.expires_aton sign-in,expires_inon refresh). Short-lived tokens now refresh at most once per half-lifetime.
Map<refreshToken, Promise>so concurrent callers shareone POST; the entry is cleared in
finally.AbortSignal.timeout()on both the discovery and token-endpoint fetches.invalid_grant) → refresh token is dead →RefreshTokenError. 5xx / network / timeout → transient: log, keep the existing token,and only fail the session if the access token has already expired. Same rule when the IdP
granted no refresh token at all.
src/server/auth.tssession: { strategy, maxAge, updateAge }+jwt: { maxAge }.authorized()now returnsfalsewhensession.error === "RefreshTokenError", even with auser present.
session()is unchanged — still blanksaccessTokenon that error and stillexposes
idToken,expiresAt,roles, anderror.Four new optional
AppLayerAuthConfigfields, each also readable from an env var(precedence: explicit value > env var > default; non-numeric/non-positive falls back to default):
sessionMaxAgeSecondsAUTH_SESSION_MAX_AGE_SECONDS28800(8 h)sessionUpdateAgeSecondsAUTH_SESSION_UPDATE_AGE_SECONDS900refreshSkewSecondsAUTH_TOKEN_REFRESH_SKEW_SECONDS300oidcTimeoutMsOIDC_HTTP_TIMEOUT_MS10000Client —
SessionProvidertakes arefetchIntervalprop (default 120,refetchOnWindowFocuskept), forwarded from
OERootLayoutassessionRefetchInterval, so an app whose tokens liveunder two minutes can poll more often.
Behavioural changes for consumers
the 30-day cookie will see one IdP round-trip per working day.
/logininstead of rendering ashell whose API calls 401.
Both are documented in
docs/releases/upgrade-to-0.7.1.md.Tests
src/server/__tests__/oidc-tokens.test.ts— 24 cases, covering:needsRefresh()false right after a 60 s token is issued and true past half its lifetime; truewhen
expiresAtis missing; two concurrent refreshes issue exactly one POST; 400invalid_grantsets the error; 503 with a still-valid token does not set it and keeps
accessToken; 503 with anexpired token does;
authorized()false onRefreshTokenErrordespite a user; discovery fetchedonce across refreshes within the TTL; plus option-precedence and skew-clamping cases.
Full suite 60 passed (9 files);
typecheck,lint,build, andformat:checkall green.Notes
@types/node, so env lookup goes through a typedglobalThis.process?.envaccessor rather than adding a dependency.
0.7.1—open-taskscan bump its dependency from^0.7.0.