IdentityServer
IdentityServer copied to clipboard
Consider providing an absolute expiration feature for the cookie
The built-in ExpiresUtc on the AuthenticationProperties is used for inactivity, but there is no built-in AbsoluteExpiresUtc. Consider adding such a feature. Likely would be a new/custom entry in the Items collection, and the easiest place for us to validate would be in our IdentityServerAuthenticationService in AuthenticateAsync -- we'd call the inner, and check the result's Items, and return AuthenticateResult.Fail if we're past the absolute expiration.
But all of this is possible today for customers -- mainly by adding the same item above, and then handling the CookieAuthenticationEvents's OnValidatePrincipal event.
Would be a nice feature to have, we have now implemented it ourselves (for BFF)
Was just looking for this feature this week. I went ahead and used your insight to build it out with the ValidatePrincipal event. Would be great to see IdSrv support this out of the box, but it's very easy to do already. Thanks!
@brockallen
Just a follow-up on this. ValidatePrincipal
can work, but it appears tricky to implement correctly. I don't feel I have full grasp of it all yet. You also need to manually call session management to ensure back-channel requests are handled. Calling HttpContext.SignOutAsync()
with the IdSrv cookie scheme is not enough and can cause additional problems from within ValidatePrincipal()
Code from Duende in their middleware:
context.Response.OnStarting(async () =>
{
if (context.GetSignOutCalled())
{
_logger.LogDebug("SignOutCalled set; processing post-signout session cleanup.");
// this clears our session id cookie so JS clients can detect the user has signed out
await userSession.RemoveSessionIdCookieAsync();
var user = await userSession.GetUserAsync();
if (user != null)
{
var session = new UserSession
{
SubjectId = user.GetSubjectId(),
SessionId = await userSession.GetSessionIdAsync(),
DisplayName = user.GetDisplayName(),
ClientIds = (await userSession.GetClientListAsync()).ToList(),
Issuer = await issuerNameService.GetCurrentAsync()
};
await sessionCoordinationService.ProcessLogoutAsync(session);
}
}
});
The problem is that IUserSession
cannot get the user since we are technically not authenticated and it tries to call AuthenticateAsync during the response. We are somewhat fortunate that MS ensures that AuthenticateAsync is only called once on a handler or we could end up in a loop.
This is what I have so far to get past these issues:
e.g.
We have an appsetting for CookieTicketAbsoluteExpirationInSeconds
.
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// check the absolute ticket expiration
if (context.Principal.IsAuthenticated() && _identityOptions.Authentication.CookieTicketAbsoluteExpirationInSeconds > 0)
{
// for backwards-compatibility with currently authenticated users,
// properties may not have an absolute expiration yet and will be null
var absExpiration = context.Properties.GetItem<DateTimeOffset?>(AuthConstants.Items.AbsoluteExpirationUtc);
if (absExpiration.HasValue && DateTimeOffset.UtcNow > absExpiration)
{
// first delete all auth cookies
await context.HttpContext.SignOutOfEnterpriseIdentity(); // our own ext helper for HttpContext.SignOutAsync
// make sure Duende coordinates back-channel logout requests
context.HttpContext.Items.Remove("idsvr:IdentityServerSignOutCalled"); // un-set this so Duende doesn't try to handle session coordination during the response
var issuer = await _issuerNameServiceFactory().GetCurrentAsync();
var session = new UserSession
{
SubjectId = context.Principal!.GetSubjectId(),
SessionId = context.Principal!.GetSessionId(),
DisplayName = context.Principal!.GetDisplayName(),
ClientIds = DuendeAuthExtensions.GetClientList(context.Properties).ToList(),
Issuer = issuer
};
await _sessionCoordinationServiceFactory().ProcessLogoutAsync(session);
// un-set the principal
context.RejectPrincipal();
}
}
}
Does this look like a reasonable approach?
Does this look like a reasonable approach?
Really can't say until I've dug into the same level myself. The call to Remove("idsvr:IdentityServerSignOutCalled")
looks janky to me, so I'd want to look for a better/proper approach.
That's understandable. At first this appeared very straight forward to implement, but I had forgotten all about session coordination/back-channel. Will continue to do more testing on my implementation. The call to Remove("idsvr:IdentityServerSignOutCalled")
is technically not needed since MS will memoize the original cookie handler's authentication result and DefaultUserSession.GetUserAsync()
will just return a null principal.
Maybe as a sample, @josephdecock?