How do I enforce 2FA in .Net Core Identity? - asp.net-core

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

Related

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

Can we restrict users in identity server4 to specific applications?

I am trying to implement IdentityServer 4 for enterprise scenario.
I understand that users are registered against Identity server.
My question is how to give permissions to users against applications, like as users are needed to assign to a particular application, if not assigned application should return unauthorized.
If a user needs to access multiple applications then multiple assignments are needed.
I am looking a way for Identity server to invalidate the submitted token if the user doesn't have access to the application in a single go, even though the challenged token might be valid if it is submitted by other application which the user has access to
Identity Server absolutely handles authorizations on the most basic level. It creates authorization codes and access_tokens that are essential in an applications authorization. Without them you cannot get authorized. Thus for others to claim Identity Server does not do authorizations is flat out wrong.
I came in here a week ago looking for a solution for this very same problem. I want to restrict users to specific applications by not granting them access tokens if they fail to meet certain parameters, in my case a UserClient table. Lucky for you I have a solution. Identity Server 4 implements a few, what they call, CustomValidators that occur at the time of authorization or token creation. They are
internal class DefaultCustomAuthorizeRequestValidator : ICustomAuthorizeRequestValidator
internal class DefaultCustomTokenRequestValidator : ICustomTokenRequestValidator
public class DefaultCustomTokenValidator : ICustomTokenValidator
There name really says it when they get called. Each one contains a single method
public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
{
return Task.CompletedTask;
}
Notice something? That's is right! It does nothing. Almost as if they are meant to be replaced. (It is).
This is the area that you can add your custom logic to reject the request. CustomAuthorizeRequestValidationContext contains ClientId and User claim information. It also contains a boolean value called IsError. Simply set that to true and whamy! Access denied. You can also set error messages etc. Here is an example that implements the ICustomAuthorizeRequestValidator inface that will restrict a user based on there user Id
public Task ValidateAsync(CustomAuthorizeRequestValidationContext context)
{
var sub = context.Result.ValidatedRequest.Subject.FindFirst("sub");
if (sub != null && sub.Value != "88421113")
{
context.Result.IsError = true;
context.Result.Error = "Unauthorized";
context.Result.ErrorDescription = "You are not authorized for this client";
}
return Task.CompletedTask;
}
Feel free to inject a dbcontext or two to read off of your userclient table. I check the sub claim to be null because this will get hit several times before actual login occurs.
From what I noticed all three behave similar in terms of use, but different in terms of outcome. Setting an error ICustomAuthorizeRequestValidator will prevent the redirect to your client and instead direct you to the Identity Server error screen. The other two will redirect back to the client and generally throw some throw some sort of HttpResponse error. Therefore replacing the ICustomAuthorizeRequestValidator seems to work best.
So simply created a class that implements ICustomAuthorizeRequestValidator. Then add that into your identity services like so
services.AddIdentityServer().AddCustomAuthorizeRequestValidator<MyCustomValidator>()
and you are done done.
You can add a claim in your IdentityServer4's claims table called "role" and in your application, add some UI to authorize a person via email or similar, and then set his/her role in the claims db. And you can also delete the authorized user from your application, which should un-assign a role to that particular person. Thus he/she although is successfully authenticated, can't use your application because you have authorized then. Hope this approach helps you!
For users, IdentityServer is authentication only. Authorization should be handled by your application.
Authentication = Verifying who a user is
Authorization = Verify what a user can do
Update
I wrote an article on this topic to clarify how OAuth 2.0 does is not user-level authorization. Hope it helps! https://www.scottbrady91.com/OAuth/OAuth-is-Not-User-Authorization
As Scott says, Identity Server will authenticate that the user is who they say they are, not explicitly tell you what that user can do.
You can use the claims returned as part of that authentication to then perform authorization checks within your app. For example, you might use the sub or id claims to perform checks from your app on whether the user associated with that sub/id is allowed to access a specific resource.
The water gets a bit muddier when you bring role claims into the picture, but so long as you appreciate the difference between authentication and authorization you should be ok.
In our enterprise scenario we split it into layers:
We introduced a tenant -- a customer (organization) of our enterprise
solution.
Then we have roles (not more than 20 or so) assigned for
each particular user.
IdentityServer fetches users from tenant and access APIs. The only pre-check it performs is that a particular client (application), requested a token, is not restricted for the particular tenant (customer-level licensing), otherwise we display a message and block the challenge response.
Then we come to an app. With a valid token, having tenant and roles inside. The roles-to-functions assignment could be unique within the tenant. So the application itself performs a granulate permissions check, using a separate API. The application is free to enable-disable some functions or even redirect to the special page in IdSrv "Access denied for the app".
With such approach we are scalable, we are configurable, we are as fast as we want. In previous generation we had "all in one" identity+access+licensing monster-like system, and we decided to split. Today we do not face any real limits with adding new customers (tenants), having 20000 users in average each.
Another way, you can redirect user back to respective client login page it they are not assigned to application/client by using IProfileService of IdentityServer4.Services
public async Task IsActiveAsync(IsActiveContext context)
{
if (!string.Equals("MyAllowedApplicationId", context.Client.ClientId, StringComparison.OrdinalIgnoreCase))
{
context.IsActive = false;
}
}
You have to set IsActive = false to redirect user back to login page where user can login with user details which is allowed in application

IdentityServer4 - How to log in user following password reset if existing auth cookie exists?

I'm attempting to extend my Identity Server 4 implementation and provide an in house password reset feature. I've completed the entire password reset process however I'm running into a situation upon redirecting the user back to the client application that's causing me grief.
If there is a valid existing authentication cookie, for instance if a previous user doesn't initiate a proper log out, when I redirect the current user to the client application, the middle ware uses the existing cookie to build the principal which inevitable ends up with the current user being treated as the previous user, which makes sense.
In vain I attempted to make a call to sign in the current user post password reset in Identity Server via await HttpContext.Authentication.SignInAsync(...) as shown during login on their samples here with no change in results.
It seems as I'm missing something fundamental in how to properly reproduce the sign in process from within my password reset process.
Is there any way from Identity Server that I can effectively sign in the current user post password reset and force the client application to invalidate/ignore the existing authentication cookie?
Update: After thinking about this over the weekend, I guess this broaches a much broader subject in that how far does one go in ensuring the invalidation of existing auth cookies? The fact is that because of the way cookie authentication works, as long as that cookie remains valid we are under the assumption that we are is still receiving requests from the original physical person.
One might argue that this is a good case for reducing the expiration time of the cookie, but that's a slippery slope because on one hand if the expiration is too short, usability can be affected making for a less enjoyable experience and unhappy users. Make the expiration too long and that window where a valid auth cookie is sitting on the browser idle with no meat and bones behind it grows, and all it takes is someone else to browse to the site to proceed unhindered and authenticated as someone else. This would be most evident in a situation with shared computers where many people need to access the same app.
I suppose a secondary question would be to what extent is the onus on the user to initiate a proper logout process, and to what lengths should I go to to ensure any existing auth cookies are invalidated?
After a few conversations with co workers, the consensus was that it is the responsibility of the user to ensure they are properly logged out in any public/computer sharing situations and that we could not guarantee without a reasonable doubt any previously existing authentication cookies are cleaned up without greatly affecting user experience and basic site functionality.
For due diligence we however will provide some manner of attempting to clean up any existing authentication cookies by doing a simple check and redirect on the default route of the client application. This solution has nothing to do with identity server, and is not fool proof, but in conjunction with tuning the session expiration times should prove effective for daily users.
[AllowAnonymous, Route("")]
public async Task<IActionResult> Index()
{
if (User.Identity.IsAuthenticated)
{
await HttpContext.Authentication.SignOutAsync(...);
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
}
return RedirectToAction(nameof(Welcome));
}
[Authorize]
public IActionResult Welcome()
{
return View();
}

How to propagate an administrator's changes to a user's claims

Situation
Let's say an administrator of a site removes a user from the Admin role and adds her to the Contributor role. According to the site's database, that user has been demoted and should no longer have access to Admin-only features. Now the user comes back to the site some time after that change, but had logged in sometime before the change and is still logged in. So long as that user does not log out, she will continue to have claims that say she is in the Admin role. If she logs out, or gets logged out, she loses the claim that she belongs to the Admin role and when she signs back in receives the new claim of belonging to the Contributor role.
Desire
What I would like to happen, perhaps the next time the user requests a page from the site after the administrator made the change, is have that user transparently lose the Admin role claim and gain the Contributor role claim without them having to sign out or do anything special. In fact, I would prefer they are unaware of the change, except that her menu has changed a little because she can no longer perform Admin-only activities.
How would you handle this situation in a way that is invisible to the affected user?
My thoughts
I am using ASP.NET MVC 5 and ASP.NET Identity, but it seems like a solution to this could be easily generalized to other claims based frameworks that utilize cookies. I believe that ASP.NET Identity stores claims in the user's cookies by default in MVC 5 apps.
I have read the following post along with many others on SO and it comes closest to answering this question but it only addresses the case where the user updates herself, not when someone else like an administrator makes the change to her account: MVC 5 current claims autorization and updating claims
There is a feature in Identity 2.0 which addresses this, basically you will be able to do something like this which adds validation at the cookie layer which will reject users who's credentials have changed so they are forced to relogin/get a new cookie. Removing a role should trigger this validation (note that it only does this validation check after the validationInterval has passed, so the cookie will still be valid for that smaller timespan.
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider {
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});

Problems configuring user authentication by external API on Symfony2

I have a problem authenticating users for my new Symfony2 application.
This applications gets all the info through an API, so no database is used. When a user goes to login page, he introduce the user and password in the login form. Then, I have to authenticate him using an API call. This API call returns "false" if it's not a user, and return a token key and a token secret if its a correct user. With this token key and secret, during the user session, I can make all the API requests I need for rendering all the pages of the application. Once the user session is over and token key and secret are erased, the user has to login again.
I don't know really how ti implement that. I read this http://symfony.com/doc/current/cookbook/security/custom_provider.html and that http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html, and I'm still so lost... :(
Can any one help me?
Thank you so much :)
If you want to write custom authentication you have found the correct links. As an example you can see the implementation of the OAuth authorization HWIOAuthBundle. But keep in mind that this type of authentication creates a user on your system. If you do not use a database, you must make a request to the API every time user send a request.
First you need to understand that there is no magic. On every request symfony checks if url matches one of the specified firewalls (see secutity.yml). Listener that fired you can see in the firewall factory. If matches are found, the action switches to the corresponding AuthenticationListener. Listener attempts to authenticate the credewntials by creating Token, which is sended to AuthenticationProvider
$this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
in AuthenticationProvider
public function authenticate(TokenInterface $token) {
...
}
AuthenticationProvider try to get user via UserProvider. In case of success, Token stored in the session. On subsequent requests, ContextListener comes into play first, checks the session, extract token and send it to AuthenticationProvider similar.
In general terms, the scheme looks like that. More info you can find examining the source code of Symfony Security component.
Really good starting point is a UsernamePasswordFormAuthenticationListener. It just take login and password from request and make simplest UsernamePasswordToken.
protected function attemptAuthentication(Request $request)
{
...
}
Good luck!