How to use AuthenticationManager SignIn when using OpenId - authentication

We have a need to provide Mixed Mode Authentication and we've spent weeks chasing down samples, and bits and pieces and finally we have our ClaimsIdentity properly built, we just need to figure out a way to have OWIN recognize our ClaimsIdentity as the logged in user.
Everything we've found thus far is defining app.UseCookieAutentication in Startup.Auth.ConfigureAuth(). Set AuthenticationType = "Cookies", then set a login path.
However, we use a custom version of Identity Server 3 and in addition to UseCookieAuthentication, we also use app.UseOpenIdConnectAuthentication.
We've had issues with setting the UseCookieAuthentication LoginPath. We've tried putting something in it and leaving it empty.
Code we use to login our Windows users, we make calls to IdentityServer, get our tokens and claims.
We then use UserManager.CreateIdentityAsync as follows.
var _identity = await _userManager.CreateIdentityAsync(_slUser, DefaultAuthenticationTypes.ApplicationCookie);
Everything appears good here.
Then we use that Identity with the following code:
var _ctx = HttpContext.Current.GetOwinContext();
var _authenticationManager = _ctx.Authentication;
_authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, _identity);
The code runs but when we check _ctx.Authentication.User, the identity we created still isn't showing up which results in a user getting redirected to the login page on the Identity Server.
So in summary, we're trying to figure out how to take a ClaimsIdentity we've authenticated and built up in code and have it become of apart of the OwinContexts Authentication.User.Identities so it will be available throughout our application.

Related

How do I enforce 2FA in .Net Core Identity?

Question: How can I enforce existing users to set up 2FA in .Net Core 3.1 Identity?
I have seen a couple of answers here already, but I have issues with them as follows:
Redirect user to set up 2FA page on login if they do not have it set up. Problem with this is that the user can simply jump to a different url to avoid this, therefore it is not actually enforced.
Have some on executing filter that checks if the user has 2FA enbaled or not and if not redirect them to MFA set up page. The issue I have with this is that on every single navigation the server must go to the database to check whether the user has this field enabled, thus creating a significant performance hit on each request. I know one trip to the database may not sound like much but I have worked with applications where this was the norm and other things used this method, causing a pile up of pre action db queries. I want to avoid this kind of behavior unless absolutely necessary.
My current idea is to on login:
Check the users credentials but NOT log them in
userManager.CheckPasswordAsync(....)
If the credentials pass, check if the user has 2FA enabled or not. If they do, continue through login flow, if not:
Generate a user token:
userManager.GenerateUserTokenAsync(.......)
and store this along with the username in a server side cache. Then pass a key to the cached items with a redirect to the 2FA setup page, which will not have the [authorize] attribute set, allowing users not logged in to access it.
Before doing anything on the 2FA set up page, retrieve the cached items with the provied key andverify the token and username:
userManager.VerifyUserTokenAsync(......)
If this doesn't pass, return Unauthorized otherwise continue and get the current user from the supplied UserName in the url that was passed via a cache key. Also dump the cached items and key so that should the url be snatched by a dodgy browser extension it can't be used again.
Continue to pass a new cache key to new user tokens and usernames to each 2FA page to authenticate the user as they navigate.
Is this an appropriate use of user tokens? And is this approach secure enough? I'm concerned that having the user not logged in presents security issues, but I think it is necessary in order to avoid the previously mention problem of going to the database on every request to check 2FA, as with this method trying to navigate away will just redirect to login.
I implemented this via a Filter Method
I have a BasePageModel which all my pages inherit
public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
if (!User.Identity.IsAuthenticated)
{
await next.Invoke();
return;
}
var user = await UserManager.GetUserAsync(User);
var allowedPages = new List<string>
{
"Areas_Identity_Pages_Account_ConfirmEmail",
"Areas_Identity_Pages_Account_ConfirmEmailChange",
"Areas_Identity_Pages_Account_Logout",
"Areas_Identity_Pages_Account_Manage_EnableAuthenticator",
"Areas_Identity_Pages_Account_ResetPassword",
"Pages_AllowedPageX",
"Pages_AllowedPageY",
"Pages_Privacy"
};
var page = context.ActionDescriptor.PageTypeInfo.Name;
if (!user.TwoFactorEnabled && allowedPages.All(p => p != page))
{
context.Result = RedirectToPage("/Account/Manage/EnableAuthenticator", new { area = "Identity" });
}
else
{
await next.Invoke();
}
}
I then changed both the Disable2fa and ResetAuthenticator pages to redirect to the main 2fa page
public IActionResult OnGet() => RedirectToPage("./TwoFactorAuthentication");
And removed the reset/disable links from that page
I chose to implement a more modern and OAuth friendly solution (which is inline with .Net Core Identity).
Firstly, I created a custom claims principal factory that extends UserClaimsPrincipalFactory.
This allows us to add claims to the user when the runtime user object is built (I'm sorry I don't know the official name for this, but its the same thing as the User property you see on controllers).
In here I added a claim 'amr' (which is the standard name for authentication method as described in RFC 8176). That will either be set to pwd or mfa depending on whether they simply used a password or are set up with mfa.
Next, I added a custom authorize attribute that checks for this claim. If the claim is set to pwd, the authorization handler fails. This attribute is then set on all controllers that aren't to do with MFA, that way the user can still get in to set up MFA, but nothing else.
The only downside with this technique is the dev needs to remember to add that attribute to every non MFA controller, but aside from that, it works quite well as the claims are stored in the users' cookie (which isn't modifiable), so the performance hit is very small.
Hope this helps someone else, and this is what I read as a base for my solution:
https://damienbod.com/2019/12/16/force-asp-net-core-openid-connect-client-to-require-mfa/
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa?view=aspnetcore-5.0#force-aspnet-core-openid-connect-client-to-require-mfa

Can I Combine Identities from two Authentication Schemes in ASP.NET Core 3?

I have a web application that utilizes our organization's CAS servers for authentication. I am currently developing an integration with DocuSign for this application. A user will first come to our site and sign in with CAS. Then, they can go to the DocuSign area of the application, sign in to DocuSign, and then perform some functions tied to the DocuSign API. Each of these pieces works properly within my application.
My current problem is that I cannot access both Identities within the HttpContext simultaneously. I need the CAS Identity to determine user behavior and access to certain functions. I need the DocuSign Identity to get the necessary values to enable the API calls. In Startup.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/Account/Login");
})
.AddCAS(options =>
{
options.CasServerUrlBase = Configuration["CasBaseUrl"];
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOAuth("DocuSign", options ->
{
//necessary DocuSign options
});
I have a HomeController that uses the Authorize attribute and properly requires CAS to access. I can properly access the claims from the CAS Identity. I have a DocusignController where I use Authorize(AuthenticationSchemes = "DocuSign"). An ActionFilter that applies to this controller shows that the DocuSign Identity is coming through properly. Unfortunately, the CAS Identity is not attached, so I cannot check for things such as admin permissions. I examined the cookies, and saw that the .AspNetCore.Cookies value changes when I go between the different Authentication Schemes.
One attempt at a solution was to change the DocusignController to have Authorize(AuthenticationSchemes = "DocuSign, CAS" as its attribute. This seems to be an 'or' rather than an 'and' of the two schemes. If CAS was the most recent scheme, then the CAS Identity is the one seen in my ActionFilter. Same for DocuSign.
Another attempt was to create my own AuthenticationHandler to combine the two Identities. Appended to the AddAuthentication above:
.AddScheme<CombinedSchemeOptions, CombinedHandler>
("Combined", op => { });
Then, within this CombinedHandler:
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var CasResult = await Context.AuthenticateAsync("CAS");
if(CasResult?.Principal != null)
{
principal.AddIdentities(CasResult.Principal.Identities);
}
var DSResult = await Context.AuthenticateAsync("DocuSign");
if(DSResult?.Principal != null)
{
principal.AddIdentities(DSResult.Principal.Identities);
}
var ticket = new AuthenticationTicket(principal, "Combined");
return AuthenticateResult.Success(ticket);
}
This seems to behave the same way as before. The most recent Scheme I've used, upon entering the Combined scheme, will give a successful Result with an Identity, and the other Result will return with a failed result.
Is it possible for me to have these two Identities within the HttpContext.User simultaneously? Is it otherwise possible for me to combine claims from the two Identities into one and pass that along?
Further research and tinkering has led me to a solution. First, I discovered that I needed the two authentication schemes to write to separate cookies. I then made sure the DocuSign scheme used the new cookie as its SignInScheme.
.AddCookie("DSCookie")
.AddOAuth("DocuSign", options =>
{
options.SignInScheme = "DSCookie";
//further DocuSign options
}
I also learned that the Authorize tag works as an OR statement (if you have CAS OR DocuSign, you are authorized for this action), yet it will pass along all valid identities permitted by the scheme (You have CAS and you have DocuSign? Bring both Identities in with you). Thus, the second part of my solution has a single action that requires DocuSign authentication, prompting the necessary challenge. Then, I move to actions that have both authentication schemes, and both Identities are brought along properly.
EDIT: To elaborate per Dmitriy's request
[Authorize(AuthenticationSchemes = "DocuSign")]
public ActionResult Index()
{
return RedirectToAction("Control");
}
[Authorize(AuthenticationSchemes = "DocuSign,CAS")]
public ActionResult Control()
{
//relevant code
}
I click on a link to go to the Index action. Since I currently do not have a valid DocuSign Identity, it performs the necessary prompt and takes me to the sign in screen. If I linked directly to Control, Authentication would see that I currently had a valid CAS Identity, so would approve my access, even though I did not yet have the DocuSign Identity.
After the sign in, when I arrive at the Index function, only the DocuSign Identity is currently available. I then redirect to Control. That function verifies I have at least one of the specified Identities, grants me access, and passes along any of the listed Identities I currently possess. Thus, inside Control, I can grab information from both CAS and DocuSign.

How to identify user when client makes calls?

I have custom cookie authentication working. I think I can also get server-side authorization working. The trouble I am having is with client-side authorization. (IE: how does the client know what can or can't be accessed?)
So, for example, say there is a feature that is restricted to only some users. In my user data, I can have a flag that indicates which users can or can't access the feature. I can then set a role value at login indicating that users that can access that feature belong to a particular role. I can then set an Authorize attribute on the handler method to say that only users with that role can access that handler.
This prevents unauthorized users from calling that handler (theoretically; I have yet to test that but it seems like it should work). But, client-side, where I am building the UI, how can I know whether to add that feature into the UI or not?
I could return a UserInfo object to the client-side, telling the client what they are or aren't allowed to do, and then build the UI accordingly. The problem with this is that it requires saving that data between page refreshes. I would have to put it in local storage, or indexed DB, or something like that.
I could have an AJAX handler that lets the client ask whether it is allowed to do something. I think this seems like the best approach, but then I have to somehow identify the user. Again I would be left with the problem that the user needs to remember some user information (at least a username or userID) in order to identify themselves to the AJAX method.
It seems like this is exactly what the Authentication Cookie value is for. And indeed, as far as I know it is sent with every request already. So all I would need to do is retrieve that value server-side, once at login, so that I can remember which auth cookie is associated with which user (and so can or can't access certain features), and then retrieve it on future AJAX calls, to compare with the user database.
So the question then is, how do I retrieve this value? I think that I can retrieve it from a future request via something like:
var authKey = HttpContext.Request.Cookies[MyAuthCookieName];
But how do I remember it in the first place when it is generated? Apparently I cannot read Response cookies; they can only be added or deleted. How do I get the uniquely identifying value from the auth cookie when it is generated? It should be something like this, but it doesn't work:
var claims = new List<Claim> {
new Claim(ClaimTypes.Name, username)
};
if (canAccessFeature)
claims.Add(
new Claim(ClaimTypes.Role, "FeatureRole")
);
var identity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme
);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties {
IsPersistent = true
}
);
var cookie = HttpContext.Response.Cookies[MyAuthCookieName]; // ERROR: cannot index
Ok I realized that I can access the user information via the User property on the PageModel. You can use the IsInRole method to test whether you added the user to a role.
Rather than save information client-side you can just have a handler that tells you if you are in a role, so that the client can know whether to build the UI one way or the other:
public IActionResult OnGetCanUseFeature() =>
new JsonResult(
User.IsInRole("FeatureRole")
);
For protecting the actual handler, note that the [Authorize] attribute does not work on Razor pages at the handler level. But you can simply ask if the user is in a role, and can return a 403 status result if not:
public IActionResult OnPostRestrictedFeature() {
if (!User.IsInRole("FeatureRole"))
return new StatusCodeResult(403);
...

Missing Claims from within the IdentityServer Website, including all samples

I am sure this is down to a lack of understanding.
I am trying to access the currently-logged in users claims, within an IdentityServer instance. I am finding that any claims I provide the user are only available to the setup clients, and not the IdentityServer itself.
My issue can be replicated by using any of the quick start samples provided by the IdentityServer4 team (QuickStart Samples)
I am building a site that will provide authentication, using IdentityServer4, and also provide some interface screens to manage your own profile. To facilitate this I will need access to the claims from within the IdentityServer site.
If we look at the test users on the quick starts, we have this user:
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
We can see it has 2 claims; name and website.
Within the login controller, I also add another claim, just before signing in (by way of experimenting)
user.Claims.Add(new Claim("given_name", "bob"));
// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
When the QuickStart site and the MVC Client are running, I can successfully log in. The Secure page then shows me the claims below (after enabling AlwaysIncludeUserClaimsInIdToken)
However, if i visit the Grants section of the IdentityServer4 Quickstart, and inspect the current User I see an entirely different set of claims, shown below:
How, within IdentityServer4 Quickstart, can i access the same list of claims that were returned in the ID Token?
My specific reason is i will be storing an Active Directory UPM as one of the claims and will need access to this when the user is within any secure page in our Identity Server.
Ok - after a day of playing around, I realized there were other overrides for the HttpContext.SignInAsync() method.
Before, I had this - as per tutorial
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
Changing this to
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
Gives me exactly what i was looking for.
Leaving this here on the off chance others have the same issue.

Should Name Claim remain omitted in JWT Access Token with IdentityServer4 (using Identity) and ASP.Net Core API?

While configuring my IdentityServer4 (using Identity) resource owner grant flow with an asp.net core API backend, I got to thinking that perhaps the "Name" claim should remain omitted in the JWT access token for user security? This claim is not available with out of the box behavior of IS4.
Previously, I had been adding in the "Name" claim for the access token in my IS4 Config.cs file as follows:
var claims = new List<string>
{
JwtClaimTypes.Name
};
return new List<ApiResource>
{
new ApiResource("api1", "Auth API", claims)
};
I was doing this because it allows a straightforward approach to get a logged in user's ClaimsPrincipal.Identity.Name for user look up inside a Controller action.
var name = User.Identity.Name;
var user = await _userManager.FindByNameAsync(name);
However, IS4 access tokens (when using Identity) include the user's GUID id in the "Sub" claim. With this, we can also look up a user using the following:
var userId = User.Claims.FirstOrDefault(u => u.Type == "sub").Value;
var user = await _userManager.FindByIdAsync(userId);
I know there is slightly more processing with the LINQ query (hardly anything tbh), but I was thinking it might be of worth to protect a user's username (email address in my situation) if an access token ever fell into the wrong hands. Especially since JWT's are so easy to decode with the likes of jwt.io.
Do you guys agree or disagree? Or am I looking at this the wrong way and missing something?
JWT usually contain the public data and it is self-contained. i.e. You don't need to communicate with a backend server to construct user's identity. You should prevent the token fell into wrong hand by using https. Also, you should balance your token validity window(usability vs security) and use a nonce for maximizing the security.
I don't think 'name' should be omitted from claim collection. A valid use-case for what you are doing is that you need to make sure that changes to your user store immediately reflect in your web API. In the case of a self-contained token, if you change the 'name' in the data store, the user will not see that change until he was issued a new token. In this case use of a 'reference token' might be a good option.
Also, It looks like you are directly accessing user store from the web API. While you might have valid reasoning behind this, Idea of using token based authentication is to delegate authentication to external party(Identity Server). So common pattern is to
Include every public data that you require in the web API in the
access token.
If token getting too big, include a subset of claims in the token and query user info endpoint when required.
Use reference tokens if you have valid reasons to do so. But this will affect the performance as it will require back channel communication with identity server.