Problems with Identity.TwoFactorRememberMe and updated user SecurityStamp - asp.net-identity-3

After signing in using two-factor with authenticator app, I set rememberClient to true like this:
await _signInManager.TwoFactorAuthenticatorSignInAsync(code: request.Code,
isPersistent: request.IsPersistent,
rememberClient: request.RememberClient);
Signing in works fine and I get the .AspNetCore.Identity.Application and Identity.TwoFactorRememberMe cookies. If I sign out and in again I don't need to use two-factor. So long everything is fine.
The problem is when I do some changes in the user, like the phone number, and the SecurityStamp is changed. After the change is made I use await _signInManager.RefreshSignInAsync(user). But the Identity.TwoFactorRememberMe cookie isn't updated. This results in two problems:
The next time I sign in I have to use the two-factor authentication again.
During the same session, if I check if the user has remembered the browser, using await _signInManager.IsTwoFactorClientRememberedAsync(user), it will result in an error "Failed to validate a security stamp" and the .AspNetCore.Identity.Application will be removed.
I've tried to renew the Identity.TwoFactorRememberMe cookie at the same time as the .AspNetCore.Identity.Application cookie, like this:
await base.RefreshSignInAsync(user);
await RememberTwoFactorClientAsync(user);
It works, but it will also set the Identity.TwoFactorRememberMe cookie for those who didn't have it before. I can't check if it is set before, because then I get the error I described in (2) above.
The next thing I will try is to do something like this for every place I do something which changes the user SecurityStamp:
var isTwoFactorClientRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user);
// do the changes...
await _signInManager.RefreshSignInAsync(user);
if (isTwoFactorClientRememberedAsync)
await _signInManager.RememberTwoFactorClientAsync(user);
Is there something I'm missing here, or is this the only way to go?
I'm using IdentityServer4 and a SPA app, but I don't believe that has anything to do with the problem.

I ended up adding a method in my custom ApplicationSignInManager:
public async Task<TResult> KeepSignInAsync<TResult>(ApplicationUser user, Func<Task<TResult>> func)
{
var isTwoFactorClientRemembered = await IsTwoFactorClientRememberedAsync(user);
var result = await func();
await RefreshSignInAsync(user);
if (isTwoFactorClientRemembered)
await RememberTwoFactorClientAsync(user);
return result;
}
When I change something which will update the user SecurityStamp I use it like this:
var result = await _signInManager.KeepSignInAsync(user, () => _userManager.SetPhoneNumberAsync(user, phoneNumber));

Related

Aspnet core cookie [Authorize] not redirecting on ajax calls

In an asp.net core 3.1 web app with cookie-based authorization I have created a custom validator which executes on the cookie authorization's OnValidatePrincipal event. The validator does a few things, one of those is check in the backend if the user has been blocked. If the user has been blocked, The CookieValidatePrincipalContext.RejectPrincipal() method is executed and the user is signed out using the CookieValidatePrincipalContext.HttpContext.SignOutAsyn(...) method, as per the MS docs.
Here is the relevant code for the validator:
public static async Task ValidateAsync(CookieValidatePrincipalContext cookieValidatePrincipalContext)
{
var userPrincipal = cookieValidatePrincipalContext.Principal;
var userService = cookieValidatePrincipalContext.GetUserService();
var databaseUser = await userService.GetUserBySidAsync(userPrincipal.GetSidAsByteArray());
if (IsUserInvalidOrBlocked(databaseUser))
{
await RejectUser(cookieValidatePrincipalContext);
return;
}
else if (IsUserPrincipalOutdated(userPrincipal, databaseUser))
{
var updatedUserPrincipal = await CreateUpdatedUserPrincipal(userPrincipal, userService);
cookieValidatePrincipalContext.ReplacePrincipal(updatedUserPrincipal);
cookieValidatePrincipalContext.ShouldRenew = true;
}
}
private static bool IsUserInvalidOrBlocked(User user)
=> user is null || user.IsBlocked;
private static async Task RejectUser(CookieValidatePrincipalContext context)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
And here is the setup for the cookie-based authorization:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(co =>
{
co.LoginPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Login)}";
co.LogoutPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Logout)}";
co.ExpireTimeSpan = TimeSpan.FromDays(30);
co.Cookie.SameSite = SameSiteMode.Strict;
co.Cookie.Name = "GioBQADashboard";
co.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = UserPrincipalValidator.ValidateAsync
};
co.Validate();
});
This actually gets called and executed as expected and redirects the user to the login page when they navigate to a new page after having been blocked.
Most of the views have ajax calls to api methods that execute on a timer every 10 seconds. For those calls the credentials also get validated and the user gets signed out. However, after the user has been signed out, a popup asking for user credentials appears on the page:
If the user doesn't enter their credentials and navigate to another page, they get taken to the login page as expected.
If they do enter their credentials, they stay logged in, but their identity appears to be their windows identity...
What is going on here? What I would really want to achieve is for users to be taken to the login page for any request made after they have been signed out.
I have obviously misconfigured something, so that the cookie-based authorization doesn't work properly for ajax requests, but I cannot figure out what it is.
Or is it the Authorization attribute which does not work the way I'm expecting it to?
The code lines look good to me.
This login dialog seems to be the default one for Windows Authentication. Usually, it comes from the iisSettings within the launchSettings.json file. Within Visual Studio you'll find find the latter within your Project > Properties > launchSettings.json
There set the windowsAuthentication to false.
{
"iisSettings": {
"windowsAuthentication": false,
}
}

How to validate that user token is valid before reset password in Asp.Net Core MVC?

I am using Asp.net core MVC for my application and I implemented the reset password functionality and it is working fine. Let me show how it is implemented. When a user requests to reset the password, the application creates a token as below:
var token = _userManager.GeneratePasswordResetToken(user);
The application sends an email to the user and when the user hit the link, it comes on the page where the user reset the password. I am resetting as below:
ChangePasswordAsync(user, token, password);
It is working fine. But I want to validate the token is valid or not before changing password. Is there any way to do it?
UserManager has a public method, VerifyUserTokenAsync, which is likely what you're after. If you check the source code here, you'll see how this is used inside of ResetPasswordAsync:
VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, ResetPasswordTokenPurpose, token)
So, it relies on a magic string, but you should be able to use the same value to get a true or false value, something like:
if(await _userManager.VerifyUserTokenAsync(user, _userManager.Options.Tokens.PasswordResetTokenProvider, "ResetPassword", token))
{
await _userManager.ResetPasswordAsync(user, token, password);
}
else
{
// handle a bad token however you see fit...
}
This untested, so I can't promise it works as-is.
Token will be validated by the Identity Framework. So I guess you don't need to do that manually.
var result = await _userManager.ResetPasswordAsync(user, token, newPassword);
Result will have a descriptive message if it fails.
Cheers,

ASP.NET MVC 5 Identity - How can I keep a user logged in after changing their password?

I'm building my first ASP.NET MVC app and I would like the user to remain logged in whenever they change their password and are redirected to the home page.
I am using SecurityStamp to enable 'Log Off Everywhere' when the user signs in somewhere else. I have the validateInterval set to 0. So far this functionality works fine, if I log in using another browser the original session logs out when another request is made.
The issue I am having now is that I would like the user to remain logged in and be redirected to the home screen (not the login page) when they update their password. Is it possible to do this?
My change password controller looks like:
public async Task<ActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user != null)
{
await UserManager.UpdateSecurityStampAsync(user.Id);
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// go to welcome page
}
}
}
I am confused about the order in which I need to change the password, update the security stamp, and sign the user in. Do I also need to sign them out before signing them back in?

How to implement OAuth2 for a single tool, without using it as my application's authorization solution

I currently have a MVC site, in .NET Core, backed by a public API. My users must log in (there are no [Anonymous] controllers), and authentication is already successfully being done using the DotNetCore.Authentication provider. All that is well and good.
What I'm now trying to do (by user request) is implement functionality for a user to read and view their Outlook 365 calendar on a page within my site. It doesn't seem too hard on the surface... all I have to do is have them authenticate through microsoftonline with my registered app, and then -- once they have given approval -- redirect back to my app to view their calendar events that I am now able to pull (probably using Graph).
In principle that seems really easy and straightforward. My confusion comes from not being able to implement authentication for a single controller, and not for the entire site. All of the OAuth2 (or OpenID, or OWIN, or whatever your flavor) examples I can find online -- of which there are countless dozens -- all want to use the authorization to control the User.Identity for the whole site. I don't want to change my sitewide authentication protocol; I don't want to add anything to Startup.cs; I don't want anything to scope outside of the one single controller.
tldr; Is there a way to just call https://login.microsoftonline.com/common/oauth2/v2.0/authorize (or facebook, or google, or whatever), and get back a code or token that I can use for that user on that area of the site, and not have it take over the authentication that is already in place for the rest of the site?
For anybody else who is looking for this answer, I've figured out (after much trial and error) how to authenticate for a single user just for a short time, without using middleware that authenticates for the entire application.
public async Task<IActionResult> OfficeRedirectMethod()
{
Uri loginRedirectUri = new Uri(Url.Action(nameof(OfficeAuthorize), "MyApp", null, Request.Scheme));
var azureADAuthority = #"https://login.microsoftonline.com/common";
// Generate the parameterized URL for Azure login.
var authContext = GetProviderContext();
Uri authUri = await authContext.GetAuthorizationRequestUrlAsync(_scopes, loginRedirectUri.ToString(), null, null, null, azureADAuthority);
// Redirect the browser to the login page, then come back to the Authorize method below.
return Redirect(authUri.ToString());
}
public async Task<IActionResult> OfficeAuthorize()
{
var code = Request.Query["code"].ToString();
try
{
// Trade the code for a token.
var authContext = GetProviderContext();
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(code, _scopes);
// do whatever with the authResult, here
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
return View();
}
public ConfidentialClientApplication GetContext()
{
var clientId = "OfficeClientId;
var clientSecret = "OfficeClientSecret";
var loginRedirectUri = new Uri(#"MyRedirectUri");
TokenCache tokenCache = new MSALSessionCache().GetMsalCacheInstance();
return new ConfidentialClientApplication(
clientId,
loginRedirectUri.ToString(),
new ClientCredential(clientSecret),
tokenCache,
null);
}
I don't know if that will ever be helpful to anybody but me; I just know that it's a problem that doesn't seem to be easily solved by a quick search.

Change e-mail or password in AspNetUsers tables in ASP.NET Identity 2.0

I have implemented the ForgotPassword (with token reset) into my MVC 5 application. We are in production. Although this works in majority of the cases, many of our end-users are of older age and get confused when they cannot login and need a reset. So in those situations, I am considering giving one of our admin staff the ability to reset a user's password and giving them the new password on the phone. The data is not that sensitive.
I tried this:
public ActionResult ResetPassword()
{ UserManager<IdentityUser> userManager =
new UserManager<IdentityUser>(new UserStore<IdentityUser>());
var user = userManager.FindByEmail("useremail.samplecom");
userManager.RemovePassword(user.Id);
userManager.AddPassword(user.Id, "newpassword");
}
I get a cryptic error stating Invalid Column EMail, Invalid Column Email Confirmed ......
I also tried the userManager.ResetPassword(), but abandoned that idea because it needs a token reset. I want to bypass it.
What am I not seeing?
Thanks in advance.
I also tried the userManager.ResetPassword(), but abandoned that idea because it needs a token reset. I want to bypass it.
How about you just generate the token and pass it to the Reset routine ?
var userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
var code = await userManager.GeneratePasswordResetTokenAsync("username");
var result = await userManager.ResetPasswordAsync("username", code, "your new password");
if (!result.Succeeded)
{
//password does not meet standards
}
The idea here is you are just emulating/bypassing the usual routine of sending the token to the client (via email) and having the link that they click on call ResetPasswordAsync
I'm not completely sure if this will work in your implementation but I use the following code with success in a use case which has basically the same requirements as yours. The difference is that I'm not letting any user reset it's own password. This is always the task of an admin.
I'm bypassing the ApplicationUserManager and edit the information directly in the table, using just Entity Framework.
// I created an extension method to load the user from the context
// you will load it differently, but just for completeness
var user = db.LoadUser(id);
// some implementation of random password generator
var password = General.Hashing.GenerateRandomPassword();
var passwordHasher = new Microsoft.AspNet.Identity.PasswordHasher();
user.PasswordHash = passwordHasher.HashPassword(password);
db.SaveChanges();
You have to get the user from the database and generate the code not by username :
public async Task<Unit> ResetPassword(string userName, string password)
{
if (!string.IsNullOrWhiteSpace(userName))
{
var returnUser = await _userManager.Users.Where(x => x.UserName == userName).FirstOrDefaultAsync();
var code = await _userManager.GeneratePasswordResetTokenAsync(returnUser);
if (returnUser != null)
await _userManager.ResetPasswordAsync(returnUser, code, password);
}
return Unit.Value;
}

Resources