User.Identity fluctuates between ClaimsIdentity and WindowsIdentity
I'm not sure if this will help, but this is how I've fixed this problem for me.
When I added Windows Authentication, it fluctuated between Windows and Claims identities. What I noticed is that GET
requests get ClaimsIdentity
but POST
requests gets WindowsIdentity
. It was very frustrating, and I've decided to debug and put a breakpoint to DefaultHttpContext.set_User
. IISMiddleware
was setting the User
property, then I've noticed that it has an AutomaticAuthentication
, default is true
, that sets the User
property. I changed that to false, so HttpContext.User
became ClaimsPrincipal
all the time, hurray.
Now my problem become how I can use Windows Authentication. Luckily, even if I set AutomaticAuthentication
to false
, IISMiddleware
updates HttpContext.Features
with the WindowsPrincipal
, so var windowsUser = HttpContext.Features.Get<WindowsPrincipal>();
returns the Windows User on my SSO page.
Everything is working fine, and without a hitch, there are no fluctuations, no nothing. Forms base and Windows Authentication works together.
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});
Turns out the problem was the ClaimsPrincipal support multiple identities. If you are in a situation where you have multiple identities, it chooses one on its own. I don't know what determines the order of the identities in the IEnumerable but whatever it is, it apparently does necessarily result in a constant order over the life-cycle of a user's session.
As mentioned in the asp.net/Security git's Issues section, NTLM and cookie authentication #1467:
Identities contains both, the windows identity and the cookie identity.
and
It looks like with
ClaimsPrincipals
you can set astatic Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity>
calledPrimaryIdentitySelector
which you can use in order to select the primary identity to work with.
To do this, create a static method with the signature:
static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)
This method will be used to go over the list of ClaimsIdentity
s and select the one that you prefer.
Then, in your Global.asax.cs set this method as the PrimaryIdentitySelector
, like so:
System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;
My PrimaryIdentitySelector
method ended up looking like this:
public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
//check for null (the default PIS also does this)
if (identities == null) throw new ArgumentNullException(nameof(identities));
//if there is only one, there is no need to check further
if (identities.Count() == 1) return identities.First();
//Prefer my cookie identity. I can recognize it by the IdentityProvider
//claim. This doesn't need to be a unique value, simply one that I know
//belongs to the cookie identity I created. AntiForgery will use this
//identity in the anti-CSRF check.
var primaryIdentity = identities.FirstOrDefault(identity => {
return identity.Claims.FirstOrDefault(c => {
return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
c.Value == StringConstants.Claim_IdentityProvider;
}) != null;
});
//if none found, default to the first identity
if (primaryIdentity == null) return identities.First();
return primaryIdentity;
}
[Edit]
Now, this turned out to not be enough, as the PrimaryIdentitySelector
doesn't seem to run when there is only one Identity
in the Identities
list. This caused problems in the login page where sometimes the browser would pass a WindowsIdentity when loading the page but not pass it on the login request {exasperated sigh}. To solve this I ended up creating a ClaimsIdentity for the login page, then manually overwriting the the thread's Principal, as described in this SO question.
This creates a problem with Windows Authentication as OnAuthenticate
will not send a 401 to request Windows Identity. To solve this you must sign out the Login identity. If the login fails, make sure to recreate the Login user. (You may also need to recreate a CSRF token)