User.Identity fluctuates between ClaimsIdentity and WindowsIdentity - c#

I have an MVC site that allows logging in using both Forms login and Windows Authentication. I use a custom MembershipProvider that authenticated the users against Active Directory, the System.Web.Helpers AntiForgery class for CSRF protection, and Owin cookie authentication middle-ware.
During login, once a user has passed authentication against Active Directory, I do the following:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
My SignOut function looks like this:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
Logging in is performed via a jQuery.ajax request. On success, the Window.location is updated to the site's main page.
Logging in with both Forms and IntegratedWindowsAuthentication (IWA) works, but I've run into a problem when logging in with IWA. This is what happens:
The user selects IWA on the login page and hits the submit button. This is sent to the regular login action via an ajax request.
The site receives the request, sees the "use IWA" option and redirects to the relevant action. 302 response is sent.
The browser automatically handles the 302 response and calls the redirect target.
A filter sees that the request is headed to the IWA login action and that User.Identity.IsAuthenticated == false. 401 response is sent.
The browser automatically handles the 401 response. If the user has not authenticated using IWA in the browser yet, they get a popup to do so (default browser behavior). Once credentials have been received, the browser performs the same request with user credentials.
The site receives the authenticated request and impersonates the user to perform a check against Active Directory. If the user passes authentication, we finalize SignIn using the code above.
User is forwarded to the site's main page.
The site receives the request to load the main page. This is where things sometimes go awry.
The User.Identity at this point is of type WindowsIdentity with AuthenticationType set to Negotiate, and NOT as I would expect, the ClaimsIdentity created in the SignIn method above.
The site prepares the main page for the user by calling #AntiForgery.GetHtml() in the view. This is done to create a new AntiForgery token with the logged in user's details. The token is created with the WindowsIdentity
As the main page loads, ajax requests made to the server arrive with ClaimsIdentity! The first POST request to arrive therefore inevitably causes an AntiForgeryException where the anti-forgery token it sent is "for a different user".
Refreshing the page causes the main page to load with ClaimsIdentity and allows POST requests to function.
Second, related, problem: At any point after the refresh, once things are supposedly working properly, a POST request may arrive with WindowsIdentity and not with ClaimsIdentity, once again throwing an AntiForgeryException.
It is not any specific post request,
it is not after any specific amount of time (may be the first/second request, may be the hundredth),
it is not necessarily the first time that specific post request got called during that session.
I feel like I'm either missing something regarding the User.Identity or that I did something wrong in the log-in process... Any ideas?
Note: Setting AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; allows the AntiForgery.Validate action to succeed whether WindowsIdentity or ClaimsIdentity are received, but as is stated on MSDN:
Use caution when setting this value. Using it improperly can open
security vulnerabilities in the application.
With no more explanation than that, I don't know what security vulnerabilities are actually being opened here, and am therefore loathe to use this as a solution.

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 a static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> called PrimaryIdentitySelector 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 ClaimsIdentitys 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)

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;
});

Related

ASP.NET Core custom AuthenticationHandler in combination with Cookies

In .NET Core 1 we could use
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AutomaticAuthenticate = true,
...
to ensure the cookie was evaluated first. On top of this we had either OAuth2 or a custom authentication module using the Basic authentication header.
This made the following flow trivial:
User reach website, gets a WWW-Authorize challenge (Bearer or Basic depending on settings)
User responds to the challenge (so for Basic, provide username/password)
As soon as the relevant middleware would validate the Bearer or Basic token on the next request, it would sign in the user - and as a result the cookie middleware would add its cookie to the respond.
On the next request the incoming cookie would be validated. If it is still valid, the user is signed in and as a result the following OAuth2 or custom middleware simply backs off and do not perform any action.
We are now trying to update that to ASP.NET Core 6 (or whatever it is called these days).
In the event handler of our custom AuthenticationHandler the following code:
context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.Principal)
does result in the cookies being set.
With
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, "MyCustomScheme")
.Build();
});
it will first validate the cookie, but it will always run "MyCustomScheme" as well. In some environments validating a username/password can add 30 seconds due to timeouts in their way too complicated infrastructure setup, and I do not see a way for my custom handler to access any previous identities allowing it to shortcut (so no, "oh, already validated by cookie, so I do not need to do anything" unless I start seriously hacking).
I could also write a "ForwardDefaultSelector" (from https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0)
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
{
var token = authorization.Substring("Bearer ".Length).Trim();
var jwtHandler = new JwtSecurityTokenHandler();
return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals("B2C-Authority"))
? "B2C" : "AAD";
}
return "AAD";
};
but checking the cookie exists is of course not enough - it might be present but expired in which case setting the only authentication schema to be the cookie would result in a failed login - even if the correct token is being passed in as well.
Am I missing some obvious way to get the cookie to authenticate if able, but if not able let another handler have a go? It seems like a simple task: Try to authenticate with Cookie, if fail, try X. 2 lines of code if I could just find a place to put those lines.
And yes, I am aware we should skip this and only support OAuth2. Unfortunately the world moves slow.

Logout from ADFS using c#

I created an asp.net webform application using ADFS. Sign in and sign out work perfectly using the default method that comes with the template.
Eg of signout button method that is included in the template
protected void Unnamed_LoggingOut(object sender, LoginCancelEventArgs e)
{
// Redirect to ~/Account/SignOut after signing out.
string callbackUrl = Request.Url.GetLeftPart(UriPartial.Authority) + Response.ApplyAppPathModifier("~/Account/SignOut");
HttpContext.Current.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
WsFederationAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
}
I have set up a timer and upon reaching zero I tried using the above code to log the user out but it doesn't work.No error thrown.
Any suggestion how to perform logout here?
What worked for me is to upon timeout to call the click event of a hidden button which in turn causes the below code to run.
// Redirect to ~/Account/SignOut after signing out.
string callbackUrl = Request.Url.GetLeftPart(UriPartial.Authority) + Response.ApplyAppPathModifier("~/Account/SignOut");
HttpContext.Current.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
WsFederationAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
ADFS is the server that is responsible for authenticating the user and for managing the user session. The website/form is just using this service. It makes sense that a site that uses this service cannot have full control over it. It would make more sense to me to log out the user from the ADFS server and have that server do the heavy lifting for you.
Note the ADFS server keeps a user logged in into ADFS server, and note that when a user requests access to a resource this manifests in an access_token. They are different things. Typically when signing somebody out with a product like identity server, in order to log out you’ll need to do two things:
Revoke the access token
Log out on the authentication server, (if
that is desired, one could argue that isn’t desirable)
Note the explicit difference between session and token. You’ll notice these concepts are also in ADFS. After a quick google search you’ll find the difference between WebSSOLifetime and TokenLifetime. I would suggest configuring those to invalidate the tokens and sessions, and thereby logging the user out after an x amount of minutes.
Hope this helps.
Have you tried the above code that you have posted directly without the timer? and did it work?
Also, Try implementing the below code and see if it works.
public void LogOut()
{
var module = FederatedAuthentication.WSFederationAuthenticationModule;
module.SignOut(false);
var request = new SignOutRequestMessage(new Uri(module.Issuer), module.Realm);
Response.Redirect(request.WriteQueryString());
}

MVC 5 Reverse Proxy Authentication and make use of Roles, by using the username only

I have an MVC 5 web app. Currently the application is using Individual Accounts to login. There are also roles in place for authorization. The users login using their username and password.
The problem is like this. I need to authenticate and authorize users requests that are coming via a reverse proxy.
User make's a request to for the app (reverse proxy url) http://myapp.domain.com
Reverse proxy will make additional calls and verify that the user is authorized to access the app content. (missing from diagram for simplicity).
If everything ok with the user the call is redirected to the actual MVC application where the request will contain a header with the username.
The MVC must check that the request is coming from the reverse proxy (not a problem to do that, IP check, plus some key sent as headers.)
MVC should read the request header, get the username, authenticate it and authorize it based on the role or roles he has.
Deny the request if the request doesn't come from the reverse proxy, deny the request if the user doesn't have appropriate roles. Example user is in role Visitor but he's trying to access admin role content.
My problem is in the authentication and authorization of the request when there is present just the username.
As the application uses already username and password for authentication, I'm thinking to do the following:
The reverse proxy will send the request to https://realapp.domain.com/account/login.
In the action login from account controller I can implement the logic to read the request and get the username from the header
At this point we know that user X is authenticated because the reverse proxy and additional systems will check that. So basically all requests arriving are considered safe (from the reverse proxy server)
If the username doesn't exists within the database (first time call to the application) the MVC app will create the user with a dummy password (Password123).
If the username exists within the database then log him in using the username and the dummy password Password123 var result = await SignInManager.PasswordSignInAsync("username", "Password123", false, shouldLockout: false);
User is authenticated and authorized.
Basically my idea is to set for each user same password in order to authenticate them within the application and make use of roles.
What do you think?
Since Identity is claim based. You don't need any password or even any user object to authenticate users. So using storage in Identity is totally optional also. You just need create some claims and authorize your users based on those. Consider this simple example as a clue:
// imaging this action is called by proxy
public ActionResoult Login()
{
// this custom method extract username from header and check IP and more
var username=_myUserManager.GetUserName();
if(username!=null)
{
// optionally you have own user manager which returns roles from username
// no matter how you store users and roles
string[] roles=_myUserManager.GetUserRoles(username);
// user is valid, going to authenticate user for my App
var ident = new ClaimsIdentity(
new[]
{
// adding following 2 claim just for supporting default antiforgery provider
new Claim(ClaimTypes.NameIdentifier, username),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"),
// an optional claim you could omit this
new Claim(ClaimTypes.Name, username),
// populate assigned user's role form your DB
// and add each one as a claim
new Claim(ClaimTypes.Role, roles[0]),
new Claim(ClaimTypes.Role, roles[1]),
// and so on
},
DefaultAuthenticationTypes.ApplicationCookie);
// Identity is sign in user based on claim don't matter
// how you generated it
HttpContext.GetOwinContext().Authentication.SignIn(
new AuthenticationProperties { IsPersistent = false }, ident);
// auth is succeed, without needing any password just claim based
return RedirectToAction("MyAction");
}
// invalid user
ModelState.AddModelError("", "We could not authorize you :(");
return View();
}
Now we authorized our users without any password so we could use Authorize filter as well:
[Authorize]
public ActionResult Foo()
{
}
// since we injected user roles to Identity we could do this as well
[Authorize(Roles="admin")]
public ActionResult Foo()
{
// since we injected our authentication mechanism to Identity pipeline
// we have access current user principal by calling also
// HttpContext.User
}
Note: Your proxy must handle your apps generated cookies and deliver them to your users properly. Since your app based on cookie.
You could download Token Based Authentication Sample from my Github repo and also read my other answer which is closes to your scenario.

FormsAuthentication.SetAuthCookie doesn't work

So, I have code in controller:
FormsAuthentication.SetAuthCookie("hello", true);
throw new Exception("" + User.Identity.IsAuthenticated);
And it always displays false. What the reason can be?
The user will be authenticated on the next request only, when he starts sending the cookie back. Forms authentication happens at the start of requests only. You can change HttpContext.Current.User to a custom authenticated identity if you really need to, but you should do a redirect instead.

Does FormsAuthentication.SetAuthCookie() Require a Redirect?

After checking a user's credentials and confirming they are good, I'm using FormsAuthentication.SetAuthCookie("Username", false); to authenticate the user.
In the masterpage I then use Page.User.Identity.IsAuthenticated to make sure we're dealing with a logged in user and not a guest.
The problem lies in first setting the auth cookie. When I set the auth cookie, immediately afterwards I run a method that uses Page.User.Identity.IsAuthenticated to change the welcome message from a generic "Welcome, guest!" message to a more personal "Welcome, username!" message. This does not work until I go to another page, so I know the login process has worked, but it seems I cannot access the information I need until a refresh or a redirect happens.
Do I need to redirect the user after setting the auth cookie in order use Page.User.Identity.IsAuthenticated to change the message?
I have seen this before so I know the answer is yes. (As in, yes you do need to redirect the user to correctly use Page.User.Identity.IsAuthenticated)
What I imagine is the cause is because IsAuthenticated evaluates the current request, and when the current request first came in it was recorded as not authenticated.
What you will need to do is apply whatever logic you have in said method without the check for IsAuthenicated (make it assume true).
Now I don't know the details of your method as to suggest how to re-factor it to cope with this, but you could split out the "Do Stuff" part into a separate function which you could then call directly from you login function to bypass the authentication check.
EDIT: To back up my assumption you can read this page.
The interesting part:
The forms-authentication ticket supplies forms-authentication
information to the next request made by the browser.
I'd like to point out that there's actually a way around this (since I've never seen this said in any other question like this). You can retrieve the cookie and its data where User.Identity's information comes from without a redirect. The thing is, the cookie just hasn't been sent to the browser yet.
It simply gets the cookie made by FormsAuthentication from the Response.Cookies object:
HttpCookie EncryptedCookie = Response.Cookies.Get(FormsAuthentication.FormsCookieName);
FormsAuthenticationTicket DecryptedCookie;
try {
DecryptedCookie = FormsAuthentication.Decrypt(EncryptedCookie.Value);
} catch (ArgumentException) {
// Not a valid cookie
return false;
}
// DecryptedCookie.Name: The Username
// DecryptedCookie.UserData: Any additional data, as a string. This isn't normally used
return !DecryptedCookie.Expired;

Categories

Resources