I've been banging my head against a wall for some time now about this:
I have an ASP.NET MVC 5.2.3 web application with ASP.NET Identity 2.2.1. I want to force users to
validate their email-address and
validate their mobile phone number.
So when a user registers for the application an emailVerification token is generated and sent to the user.
After that the user is redirected to the VerifyPhoneNumber endpoint in the Manage controller. SMS-code is generated and gets send to the user. User is promted to enter the SMS-code. Code is verified.
BUT if then the user receives the email with the email-verification-code and click the link the token cannot no longer be verified (Invalid Token).
As far as I understand, this happens because calling UserManager.ChangePhoneNumberAsync changes the user's SecurityStamp. Email-verification works well if phone verification is not active. To be more specific, when ChangePhoneNumberAsync is not called.
Any ideas on how to prevent the SecurityStamp from changing or allow both verifications on inital registration are greatly appreciated.
Ben
VerifyPhoneNumber
public async Task<ActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var userId = User.Identity.GetUserId();
var result = await UserManager.ChangePhoneNumberAsync(userId, model.PhoneNumber, model.Code);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToAction("Index", "Home");
}
else
{
return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess });
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "Could not verify phone number.");
return View(model);
}
ConfirmEmail
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
code = HttpUtility.UrlDecode(code);
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
Related
I've scaffolded the Identity pages and I'm trying to update Login.cshtml.cs to redirect the user to a particular page upon successful login.
var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
if(User.IsInRole("Business User"))
{
return Redirect("~/BusinessDashboard");
}
return LocalRedirect(returnUrl);
}
I've also tried changing the 5th line to:
if(User.HasClaim("role", "Business User"))
I know that once this particular user is logged in, it definitely has the role claim of "Business User" because I'm printing the claims out to check them (I've added role as a claim in my startup file by configuring IdentityServer). However, when I put break points on the code above and check User, it doesn't look as though any claims are actually being assigned at this point. Perhaps this is the issue, but if so I'm not sure how to get around it?
I read somewhere else that the user isn't really defined at this point of the ASP.NET Identity workflow so I needed to use an instance of UserManager to get the user based on the username they put into the login input field and then I was able to use the user info I gained to get the roles that could then determine the redirect.
The code below is my updated version of the OnPostAsync method that's included in the Login.cshtml.cs file you get when you scaffold the Identity pages.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
var user = await _userManager.FindByNameAsync(Input.Username);
var roles = await _userManager.GetRolesAsync(user);
if (roles.Contains("Business User"))
{
return Redirect("~/BusinessDashboard");
}
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
I am using the default identity pages with some modifications, in the login page I included the username for the user to login. It works perfectly, the user now can login by both the email and username, but when the users enters false info, a null exception appears instead of showing
Invalid login attempt
Code:
if (ModelState.IsValid)
{
//Check if user entered email or username in the Input.Email property
var user = await _userManager.FindByNameAsync(Input.Email) ?? await _userManager.FindByEmailAsync(Input.Email);
// user is null if not exist? error
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(user, Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
//some code
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
I get why the error happens, but don't know what the proper way to solve it.
Originally the signInManager checks the user input not the actual user, so if the input is not found it will not be succeeded, how can I do it the same old way?
when the users enters false info, a null exception appears
In source code of PasswordSignInAsync(TUser, String, Boolean, Boolean) method, we can find it will throw NullException error if user is null.
public virtual async Task<SignInResult> PasswordSignInAsync(TUser user, string password,
bool isPersistent, bool lockoutOnFailure)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
return attempt.Succeeded
? await SignInOrTwoFactorAsync(user, isPersistent)
: attempt;
}
how can I do it the same old way?
You can modify the code as below to check if user is null, and set and display "Invalid login attempt." error message.
var user = await _userManager.FindByNameAsync(Input.Email) ?? await _userManager.FindByEmailAsync(Input.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
var result = await _signInManager.PasswordSignInAsync(user, Input.Password, Input.RememberMe, lockoutOnFailure: true);
//...
//code logic here
I would like to implement the following flow for two factor authentication in asp.net mvc:
var res = sign.PasswordSignIn("myusername", "mypassword", false, false);
if(res == SignInStatus.RequiresVerification)
sign.SendTwoFactorCode("EmailCode");
However I'm finding that the SendTwoFactorCode function is returning false and not sending the email because internally it is checking if the user is verified. See this line in the source. If I make a second request the call to SendTwoFactorCode works as I'm expecting.
Is there a way to make SendTwoFactorCode work correctly immediately after a call to PasswordSignIn?
Is there a way to make SendTwoFactorCode work correctly immediately after a call to PasswordSignIn?
Short answer: No
Suggested alternative:
The flow usually suggested in documentation is
Authenticate user via username and password.
If valid user and password, and requires additional verification because 2FA is enabled then redirect to the page to Send the code.
If user initiates the sending of the code, the code is sent and the user is then redirected to the verification page.
The second step is usually to confirm a provider, if there are multiple (ie SMS, email,...etc), to use for 2nd factor verification.
For example, the following does the redirect on RequiresVerification result
Account/Login
//...
var result = await signInManager.PasswordSignInAsync(username, password, false, false);
switch (result) {
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("VerifyCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
But since you have already determined that you are going to send the code via email then you can skip the second step and redirect directly to the verify code which can be where the code is sent and verified.
Account/VerifyCode
[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string returnUrl) {
var provider = "EmailCode";
// Require that the user has already logged in via username/password
var userId = await signInManager.GetVerifiedUserIdAsync();
if (userId == null) {
return View("Error");
}
// Generate the token and send it
if(!await signInManager.SendTwoFactorCodeAsync(provider)) {
return View("Error");
}
var model = new VerifyCodeViewModel {
ReturnUrl = returnUrl
};
return View(model);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model) {
var provider = "EmailCode";
if (!ModelState.IsValid) {
return View(model);
}
var result = await signInManager.TwoFactorSignInAsync(provider, model.Code, false, false);
switch (result) {
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("VerifyCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
This should allow the TwoFactorCookie to be included in the next request so that GetVerifiedUserIdAsync behaves as expected.
/// <summary>
/// Get the user id that has been verified already or null.
/// </summary>
/// <returns></returns>
public async Task<TKey> GetVerifiedUserIdAsync()
{
var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.TwoFactorCookie).WithCurrentCulture();
if (result != null && result.Identity != null && !String.IsNullOrEmpty(result.Identity.GetUserId()))
{
return ConvertIdFromString(result.Identity.GetUserId());
}
return default(TKey);
}
Source
Just like when you indicated
If I make a second request the call to SendTwoFactorCode works as I'm expecting.
That second request is important as it will include the cookie set in the previous request.
Reference How SignInManager checks for 2FA requirement
I was building dot net core web app but identity system does not allow me to login.I figured that if my username and email address in database would not be the same it wont logged in.Anyone knows what is going on??
I'm not sure if I understood your question correctly, but the following login method on an account controller allows logins with either the username or the password:
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> LoginAsync([FromBody]LoginPost model)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
// The user is identified either by Email or by Username
var user = await _userManager.FindByEmailAsync(model.Identifier)
?? await _userManager.FindByNameAsync(model.Identifier);
if (user == null)
{
return Unauthorized();
}
var signInResult = await _signInManager.PasswordSignInAsync(user, model.Password, true, false);
if (signInResult.Succeeded)
{
return NoContent();
}
return Unauthorized();
}
Please note the line where the user is looked up in the backing store: FindByEmail() ?? FindByUsername. This allows you to login with either username/password or email/password.
I'm using a Asp.NET MVC 5 project that came with a Bootstrap 3 theme we bought and in its login method they just look for the user based on his e-mail, the password is not validated. Login method below:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(AccountLoginModel viewModel)
{
// Ensure we have a valid viewModel to work with
if (!ModelState.IsValid)
return View(viewModel);
// Verify if a user exists with the provided identity information
var user = await _manager.FindByEmailAsync(viewModel.Email);
var hashPass = new PasswordHasher().HashPassword(viewModel.Password); // this is a line I added which gerenates a different hash everytime
// If a user was found
if (user != null)
{
// Then create an identity for it and sign it in
await SignInAsync(user, viewModel.RememberMe);
// If the user came from a specific page, redirect back to it
return RedirectToLocal(viewModel.ReturnUrl);
}
// No existing user was found that matched the given criteria
ModelState.AddModelError("", "Invalid username or password.");
// If we got this far, something failed, redisplay form
return View(viewModel);
}
The line I'm trying to insert the password validation is the if (user != null). I tried using _manager.Find(email,password) but it doesn't work.
How can I login the user with his e-mail and validate the password?
That is because you are hashing the password before trying to find the user.
Do
var user = _manager.Find(viewModel.Email, viewModel.Password);
// If a user was found
if (user != null)
{
//...other code removed for brevity.
which is the standard way to do it.
-------Try this code------
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return View("SuccessView");
case SignInStatus.Failure:
return View("LoginView");
}