I have an application that needs to support both Federation (IdP-Initiated) as well as manual authentication (standard username/password form). As such I am using .NET v4.5 System.Identity to make the application claims aware.
The issue we are seeing in dev is that anytime an AppPool recycle happens (like a recompile) and we reload the page or take any other action we get an error trying to access any of our custom claims. It's as if the user is still authenticated, but our custom claims are totally gone. In order to keep working, we need to close all instances of the browser and login again. This can obviously happen in the wild and is something we cannot have happen (horrible end user experience).
Is there something we are doing wrong or a way that we can trap/detect this condition and force the user to log back in again?
Background
In the case of a manual login, we build a CustomClaimsIdentity instance that gets passed into a new ClaimsPrincipal which is then used to create a new SessionSecuirytToken instance and then written out as follows:
var claims = CustomClaimsAuthenticationManager.BuildClaimsList( user );
var identity = new UniversalIdentity( claims, AuthenticationTypes.Password );
var principal = new ClaimsPrincipal( identity );
var token = new SessionSecurityToken( principal, TimeSpan.FromMinutes( user.Customer.SessionExp ?? 120 ) );
var sam = FederatedAuthentication.SessionAuthenticationModule;
sam.WriteSessionTokenToCookie( token );
In the case of Idp-Initiated logins, we handle the FederatedAuthentication.WSFederationAuthenticationModule.SignedIn event and perform checks to validate the supplied claims as well as build up the custom claims our app adds to the identity the same way we do for manual authentication.
You can safely cast an IPrincipal to ClaimsPrincipal:
ClaimsPrincipal cp = (ClaimsPrincipal)Thread.CurrentPrincipal;
Related
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.
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;
});
Here's my code:
var auth0 = new Auth0Client(
Properties.Settings.Default.auth0Domain,
Properties.Settings.Default.auth0ClientID);
var handle = new WindowInteropHelper(this).Handle;
var windowWrapper = new WindowWrapper(handle);
var user = await auth0.LoginAsync(
owner: windowWrapper,
scope: "openid profile",
withRefreshToken: true);
The return value contains a null refresh token. I also tried setting the device parameter to an arbitrary value, but that didn't help. I recently switched from Auth0 v3.x to v4.x. Auth0 v3.x returned a refresh token. Did this break in v4.x? If not, what do I need to do to get a refresh token?
What kind of identity providers are you using? - I tried this with Microsoft Live, and a Google account and that worked.
When you Login/Authenticate, do you get any other fields filled in the "user" result (without giving away any details/tokens).
Can you try like this i.e. not use "profile" in the scope.
var user = await auth0.LoginAsync(
owner: windowWrapper,
scope: "openid offline_access",
withRefreshToken: true,
device: "my-device");
or
var user = await auth0.LoginAsync(
owner: windowWrapper,
scope: "openid",
withRefreshToken: true,
device: "my-device");
Internally it appends "offline_access" onto the scope name if you have withRefreshToken=true that's why either of the above works.
You also need to supply an "id" individually for each of your devices so they can each be assigned their own refresh token.
When a refresh token has been issued, it will show up in the Dashboard -> in Users -> choose the User Authentication identity used to login -> then in User Details -> choose the Devices tab ... this will show how many Number of Refresh Tokens have been issued to your device.
As mentioned in the documentation, you shouldn't keep requesting a refresh_token....only do that when they've expired - (see how I requested more than I needed) - perhaps you hit the throttling limits on requesting refresh tokens...and it simply returns a "null" token instead...but still let your authentication succeed without error....a possibility?
Here's the documentation on refresh token (you probably know this already as you must have used it in v3).
https://auth0.com/docs/tokens/refresh-token#revoke-a-refresh-token-in-the-dashboard
I'm using WindowsPrincipal's IsInRole method to check group memberships in WPF and Winforms apps. I'm generating an identity token which can be for any AD user (not necessarily the user who's actually logged into the computer--depending on what I'm doing I don't necessarily authenticate, I just use the basic informational level token (I think the proper name for it is "identity token").
The first time this code is run on a particular computer the operating system generates the identity token for the user specified. That token is then used by the IsInRole function to validate group memberships. It's fast so I really like it. However, subsequent calls to create the WindowsIdentity/WindowsPrincipal reference the existing token instead of creating a new one. The only way I know how to update the token is to log out of the computer or reboot (which clears the token cache). Does anyone know a better way to reset cached identity tokens?
Example Code C#:
Using System.Security.Principal;
WindowsIdentity impersonationLevelIdentity = new WindowsIdentity("Some_UserID_That_Isn't_Me", null);
WindowsPrincipal identityWindowsPrincipal = new WindowsPrincipal(impersonationLevelIdentity);
If (identityWindowsPrincipal.IsInRole("AN_AD_GROUP")) { ...
VB:
Imports System.Security.Principal
Dim impersonationLevelIdentity = New WindowsIdentity("Some_UserID_That_Isn't_Me", Nothing)
Dim identityWindowsPrincipal = New WindowsPrincipal(impersonationLevelIdentity)
if identityWindowsPrincipal.IsInRole("AN_AD_GROUP") then...
Not sure if this may resolve your issue, try calling the dispose method of WindowsIdentity class either directly or indirectly.
using (WindowsIdentity impersonationLevelIdentity = new WindowsIdentity("Some_UserID_That_Isn't_Me", null))
{
// your code
}
Turns out I was wrong. It is caching, but it appears to be on the AD side. Eventually after I create a new identityWindowsPrincipal it gets updated to the correct group memberships.
I have a native app which i'm using in a multi-tenant scenario.
To authenticate the user -- and to get their consent on allowing this application to access Azure on their behalf -- I simply instantiate an AuthenticationContext and call AcquireTokenAsync. However I don't know how if this by default uses the AdminConsent or not? If not how can i achieve that?
Below is the sample code that i use:
AuthenticationContext commonAuthContext = new AuthenticationContext("https://login.microsoftonline.com/common");
AuthenticationResult result = await commonAuthContext.AcquireTokenAsync(resource,
clientId, replyUrl,
new PlatformParameters(PromptBehavior.Always));
No, this does not automatically invoke admin consent (even if an admin consents, they're just consenting for themselves, not for the whole tenant).
To invoke admin consent, you have to add prompt=admin_consent to the authentication request:
AuthenticationResult result = await commonAuthContext.AcquireTokenAsync(
resource,
clientId,
replyUrl,
new PlatformParameters(PromptBehavior.Auto), // <-- Important: use PromptBehavior.Auto
UserIdentifier.AnyUser,
"prompt=admin_consent"); // <-- This is the magic
Of course, you should not send all users to sign in with this, as it will fail if the user is not an admin.
See "Triggering the Azure AD consent framework at runtime": https://azure.microsoft.com/en-us/documentation/articles/active-directory-integrating-applications/#triggering-the-azure-ad-consent-framework-at-runtime