We have two ASP.NET MVC sites, one of which is acting as an IDP for the other. The IDP is using IdentityServer4. The SP is an OpenIDConnect client of the IDP.
This week, we got reports from several users that they had this happen:
- Click 'log in' on SP site. Redirected to login page on IDP, enter user+pass, redirected back to SP.
- Browse around for a bit
- Leave browser open, wander off, come back
- Browse around some more, notice you now appear to be logged out ('log in' button visible again and no access to protected resources)
- Click 'log in' again
At this point, they see our IDP's '500 error' page. Looking in the logs, we can see that the 500 error is originating in the IdentityServer4 middleware and the error message is 'idp claim is missing'.
To resolve this, tech support has been telling our users to clear their browser cookies and cache (which lets them log in again) but we really need a permanent fix. We do not know how long the 'wander off' period has to be to trigger the bug, but we assume some token somewhere is expiring. Devs have been unable to reproduce this effect locally: we can run the IDP and SP as local sites, and connect between them, but have never seen the error in this setup. This limits our ability to debug it.
The consensus on the IdentityServer4 Github is that this issue is caused by failing to include this line in startup.cs:
opts.OnRefreshingPrincipal = SecurityStampValidatorCallback.UpdatePrincipal;
We tried adding this, but it didn't help. On further investigation, we realised we were using services.AddAspNetIdentity<T>() which already does that anyway, so that change was probably redundant.
This is what UpdatePrincipal does, if this helps - reference to 'idp' bolded:
/// <summary>
/// Maintains the claims captured at login time that are not being created by ASP.NET Identity.
/// This is needed to preserve **claims such as idp**, auth_time, amr.
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public static Task UpdatePrincipal(SecurityStampRefreshingPrincipalContext context)
{
var newClaimTypes = context.NewPrincipal.Claims.Select(x => x.Type).ToArray();
var currentClaimsToKeep = context.CurrentPrincipal.Claims.Where(x => !newClaimTypes.Contains(x.Type)).ToArray();
var id = context.NewPrincipal.Identities.First();
id.AddClaims(currentClaimsToKeep);
return Task.CompletedTask;
}
Current setup on the IDP looks like this:
services.Configure<SecurityStampValidatorOptions>(opts =>
{
opts.OnRefreshingPrincipal = SecurityStampValidatorCallback.UpdatePrincipal;
});
services
.AddTransient<IUserClaimsPrincipalFactory<MemberIdentityUser>, UserClaimsFactory>()
.AddIdentityServer(
options =>
{
options.UserInteraction.LoginUrl = "/oidc/login";
options.UserInteraction.LogoutUrl = "/oidc/logout";
})
.AddAspNetIdentity<MemberIdentityUser>()
.AddInMemoryIdentityResources(new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
})
.AddDeveloperSigningCredential()
.AddSigningCredential(cert)
.AddInMemoryClients(_config.GetSection("IdentityServer:Oidc:Clients"));
What I'm thinking is that if this issue is caused by something expiring, thus leaving the user in an unfortunate half-logged-out state, then the best thing to do would be to completely log the user out before this expiry can happen. But I'm not sure exactly where to start setting lifetimes. As far as I can see, the ASP.NET auth cookie (in my browser) seems to be set to 'Session', it doesn't have an expiry time on it. We have been relying on a fairly 'default' install of IdentityServer4 up until now and not really had problems, except this one.
Does anyone know what else could cause this / have any advice on what expiry time we're running into here? I did find the Github issue where they talk about adding that UpdatePrincipal option, and they said:
"The cookie does not lose any claims when it slides, but it will lose claims if the security stamp is invalidated. Is this perhaps what you're running into?"
But as we already have that UpdatePrincipal option set, I'm not sure how relevant it can be.
Coming back to explain what was happening, in case anyone finds this:
Sure enough, 'UpdatePrincipal' was never being run, because we use Umbraco which overrides 'SecurityStampValidatorOptions' with its own values. Setting that option in Umbraco 'MemberSecurityStampValidatorOptions' fixed the issue.
You could also use Umbraco's 'AddMemberIdentity' function which would do 'ConfigureOptions' for you.