Summary
UserSession.ExpirationDateTime is supposed to track the SSO/cookie session lifetime (Client.UserCookieExpirationTimeInSeconds), set at login by BaseAuthenticateController.AddSession(). However, two separate code paths that legitimately renew tokens for an existing session fail to maintain this correctly:
RefreshTokenHandler extends the session using the wrong Client property (access-token lifetime instead of cookie lifetime).
AuthorizationCodeHandler doesn't extend/touch the session's ExpirationDateTime at all when reusing an existing SessionId.
Both issues together mean that even when following the maintainers' own documented guidance (UserCookieExpirationTimeInSeconds >> TokenExpirationTimeInSeconds, see #835 / #898), the persisted session still ends up expiring at roughly the access-token lifetime shortly after login — the long cookie value configured on the client has no lasting effect once a single refresh occurs.
Bug 1 — RefreshTokenHandler uses TokenExpirationTimeInSeconds instead of UserCookieExpirationTimeInSeconds
In Api/Token/Handlers/RefreshTokenHandler.cs:
async Task Authenticate(JsonObject previousQueryParameters, HandlerContext handlerContext, Domains.Token tokenResult, CancellationToken token)
{
...
session = await _userSessionRepository.GetById(tokenResult.SessionId, ..., token);
if (session != null && !session.IsActive()) session = null;
else
{
var currentDateTime = DateTime.UtcNow;
var expirationTimeInSeconds = GetExpirationTimeInSeconds(handlerContext.Client); // uses TokenExpirationTimeInSeconds
session.ExpirationDateTime = currentDateTime.AddSeconds(expirationTimeInSeconds);
_userSessionRepository.Update(session);
await transaction.Commit(token);
}
}
double GetExpirationTimeInSeconds(Client client)
{
return client == null || client.TokenExpirationTimeInSeconds == null
? Options.DefaultTokenExpirationTimeInSeconds
: client.TokenExpirationTimeInSeconds.Value; // <-- should be UserCookieExpirationTimeInSeconds
}
Compare with how the session is created at login, in UI/BaseAuthenticateController.cs:
protected async Task AddSession(...)
{
var expirationTimeInSeconds = GetCookieExpirationTimeInSeconds(client); // uses UserCookieExpirationTimeInSeconds
var expirationDateTime = currentDateTime.AddSeconds(expirationTimeInSeconds);
var session = new UserSession { ..., ExpirationDateTime = expirationDateTime, ... };
...
}
private double GetCookieExpirationTimeInSeconds(Client client)
{
return client == null || client.UserCookieExpirationTimeInSeconds == null
? _options.DefaultTokenExpirationTimeInSeconds
: client.UserCookieExpirationTimeInSeconds.Value;
}
So the session's expiration is initialized from UserCookieExpirationTimeInSeconds, but every subsequent refresh_token grant silently overwrites it using TokenExpirationTimeInSeconds instead — the exact same class of bug already fixed once in #835, but reintroduced specifically in the refresh path.
Bug 2 — AuthorizationCodeHandler never updates ExpirationDateTime when reusing an existing session
In Api/Token/Handlers/AuthorizationCodeHandler.cs:
async Task Authenticate(JsonObject previousQueryParameters, HandlerContext handlerContext, AuthCode authCode, CancellationToken token)
{
...
UserSession session = null;
if (!string.IsNullOrWhiteSpace(authCode.SessionId))
{
session = await _userSessionRepository.GetById(authCode.SessionId, handlerContext.Realm, token);
if (session != null && !session.IsActive()) session = null;
}
handlerContext.SetUser(user, session); // reads/validates session, never calls _userSessionRepository.Update(...)
}
Any silent renewal that goes through the authorization_code grant while reusing an existing SSO cookie/session (e.g. useSilentRefresh in angular-oauth2-oidc, or a full page reload that re-triggers /authorize) leaves the session's stored ExpirationDateTime completely untouched, even though a brand-new, fully valid access/id/refresh token is issued for that session.
Reproduction
Client config: TokenExpirationTimeInSeconds = 300 (5 min), UserCookieExpirationTimeInSeconds = 1800 (30 min).
t=0: User logs in. UserSession.ExpirationDateTime = t+1800 (30 min), per AddSession().
t=250: Client performs a refresh_token grant (proactive refresh before 5-min access token expires). RefreshTokenHandler sets UserSession.ExpirationDateTime = t+250+300 = t+550 — already far short of the intended 30-minute session, due to Bug 1.
t=300: Browser does a full page reload. The OIDC client re-triggers /authorize with the existing SSO cookie (prompt=none), which succeeds and exchanges an authorization_code for a fresh token via AuthorizationCodeHandler. Per Bug 2, UserSession.ExpirationDateTime remains frozen at t+550 — not extended at all, despite the user obtaining a perfectly valid new token.
t=560 (past the stale t+550 deadline, but well within the intended 30-minute session and within the actual token's/refresh token's real validity): Client attempts another refresh_token grant. RefreshTokenHandler calls _userSessionRepository.GetById(...), finds the session, but session.IsActive() returns false (DateTime.UtcNow > ExpirationDateTime), so session = null — the request proceeds as if there is no session, even though the user has been continuously, legitimately authenticated the entire time.
Expected behavior
UserSession.ExpirationDateTime should be driven exclusively by Client.UserCookieExpirationTimeInSeconds (never TokenExpirationTimeInSeconds/RefreshTokenExpirationTimeInSeconds), and should be refreshed/extended on every legitimate proof of continued session — both refresh_token grants (fixing GetExpirationTimeInSeconds in RefreshTokenHandler) and authorization_code grants that reuse an existing SessionId (adding an Update() call in AuthorizationCodeHandler.Authenticate).
Environment
SimpleIdServer.IdServer NuGet package, v7.0.0
Summary
UserSession.ExpirationDateTimeis supposed to track the SSO/cookie session lifetime (Client.UserCookieExpirationTimeInSeconds), set at login byBaseAuthenticateController.AddSession(). However, two separate code paths that legitimately renew tokens for an existing session fail to maintain this correctly:RefreshTokenHandlerextends the session using the wrongClientproperty (access-token lifetime instead of cookie lifetime).AuthorizationCodeHandlerdoesn't extend/touch the session'sExpirationDateTimeat all when reusing an existingSessionId.Both issues together mean that even when following the maintainers' own documented guidance (
UserCookieExpirationTimeInSeconds>>TokenExpirationTimeInSeconds, see #835 / #898), the persisted session still ends up expiring at roughly the access-token lifetime shortly after login — the long cookie value configured on the client has no lasting effect once a single refresh occurs.Bug 1 —
RefreshTokenHandlerusesTokenExpirationTimeInSecondsinstead ofUserCookieExpirationTimeInSecondsIn
Api/Token/Handlers/RefreshTokenHandler.cs:Compare with how the session is created at login, in
UI/BaseAuthenticateController.cs:So the session's expiration is initialized from
UserCookieExpirationTimeInSeconds, but every subsequentrefresh_tokengrant silently overwrites it usingTokenExpirationTimeInSecondsinstead — the exact same class of bug already fixed once in #835, but reintroduced specifically in the refresh path.Bug 2 —
AuthorizationCodeHandlernever updatesExpirationDateTimewhen reusing an existing sessionIn
Api/Token/Handlers/AuthorizationCodeHandler.cs:Any silent renewal that goes through the
authorization_codegrant while reusing an existing SSO cookie/session (e.g.useSilentRefreshinangular-oauth2-oidc, or a full page reload that re-triggers/authorize) leaves the session's storedExpirationDateTimecompletely untouched, even though a brand-new, fully valid access/id/refresh token is issued for that session.Reproduction
Client config:
TokenExpirationTimeInSeconds = 300(5 min),UserCookieExpirationTimeInSeconds = 1800(30 min).t=0: User logs in.UserSession.ExpirationDateTime = t+1800(30 min), perAddSession().t=250: Client performs arefresh_tokengrant (proactive refresh before 5-min access token expires).RefreshTokenHandlersetsUserSession.ExpirationDateTime = t+250+300 = t+550— already far short of the intended 30-minute session, due to Bug 1.t=300: Browser does a full page reload. The OIDC client re-triggers/authorizewith the existing SSO cookie (prompt=none), which succeeds and exchanges anauthorization_codefor a fresh token viaAuthorizationCodeHandler. Per Bug 2,UserSession.ExpirationDateTimeremains frozen att+550— not extended at all, despite the user obtaining a perfectly valid new token.t=560(past the stalet+550deadline, but well within the intended 30-minute session and within the actual token's/refresh token's real validity): Client attempts anotherrefresh_tokengrant.RefreshTokenHandlercalls_userSessionRepository.GetById(...), finds the session, butsession.IsActive()returnsfalse(DateTime.UtcNow > ExpirationDateTime), sosession = null— the request proceeds as if there is no session, even though the user has been continuously, legitimately authenticated the entire time.Expected behavior
UserSession.ExpirationDateTimeshould be driven exclusively byClient.UserCookieExpirationTimeInSeconds(neverTokenExpirationTimeInSeconds/RefreshTokenExpirationTimeInSeconds), and should be refreshed/extended on every legitimate proof of continued session — bothrefresh_tokengrants (fixingGetExpirationTimeInSecondsinRefreshTokenHandler) andauthorization_codegrants that reuse an existingSessionId(adding anUpdate()call inAuthorizationCodeHandler.Authenticate).Environment
SimpleIdServer.IdServerNuGet package, v7.0.0