What set's the User.Identity.Name and User.Identity.IsAuthenticated? - asp.net

I want to know what set's the user identity name and change isAuthenticatedto true.
Why is User.Identity.Name an empty string and User.Identity.IsAuthenticated false after SignInManager.PasswordSignInAsync has returned Success.
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var userIdentityNameTest = User.Identity.Name; // Empty string
var result = await SignInManager.PasswordSignInAsync(
model.Email, model.Password,
model.RememberMe, shouldLockout: false);
// result is "Success"
userIdentityNameTest = User.Identity.Name;
// userIdentityNameTest is still an empty string?
// User.Identity.IsAuthenticated is still false?
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl,
RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}

It seems that SignInManager.PasswordSignInAsync only validates entered data and run AuthenticationManager.SignIn if you are not using TwoFactorAuthentication. AuthenticationManager.SignIn in this case only set authentication cookie to response.
So, User.Identity is available in subsequent requests to your application. To get ApplicationUser by Name you can use ApplicationUserManager as follows:
UserManager.FindByNameAsync(model.Name)

Related

How can I show "Invalid login attempt" message inside bootstrap modal popup's login partial view without redirection to a new login page?

I have a bootstrap modal popup which has two partial views login and registration
When I enter a wrong passsword, it should show me "Invalid login attempt" inside the popup login partial view but it redirects me to a different new login partial view page (
How can I rectify this? I want the message "Invalid login attempt" to be shown in login popup without any redirection to another page?
Login code
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
//[ChildActionOnly]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return RedirectToAction("Index", "Home",
new { area = "Landing" });
//return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return PartialView("_Login",model);
}
}
This part of your code is not correct:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return PartialView("_Login",model);
You cannot return a PartialView as ActinoResult you would have to return a View
You need to return the same View as your Login page, but add the Model error and the Login page would display Model error, so first, here:
// if ModelStat is not valid, then the Model already has the Validation error
if (!ModelState.IsValid)
{
return View(model); // <-- returns the same View (Login View)
}
And here you need something like this:
default:
// login has failed, add the Model error and return to the same Login View
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);

User login using Email or Username gives null if user not found in ASP.NET Core MVC

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

Restrict same user to login from other PC

I am learning about ASP.NET Identity and I want to restrict same user to login on different PC (by IP address ), I can do it by Session but with ASP.NET Identity I need a suggestion. Thanks
You can get an IP Address of the user by Request.UserHostAddress. If you are on a localhost server then the result will be ::1 because that is the IPv6 result of a localhost.
You can save the IP Address to the user by adding user data to the IdentityModel class. e.g.
public class ApplicationUser : IdentityUser
{
public string IP { get; set; }
}
Then you could save the IP address to the users profile by requesting their IP on sign up. You can do this by changing the data that is saved to the DB on sign up. Do this in the Account controller Register method.
var user = new ApplicationUser { UserName = model.Email, Email = model.Email,
IP = Request.UserHostAddress };
Once that's done, you can change the login method to something like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
var user = await UserManager.FindByNameAsync(model.Email);
var IP = Request.UserHostAddress;
if (IP != user.IP)
{
result = SignInStatus.Failure;
ModelState.AddModelError("", "Log in with your original computer");
return View(model);
}
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
It should be done. Don't forget to run update-database in the package-manager console.

Get Roles from DB in HttpModule

I have to get roles of a user from DB as per my application nature. I am authenticating user in context.AcquireRequestState += context_AcquireRequestState; event handler in HttpModule. Can I do db calls from HttpModule to assign roles in Identity? Is it good practice? if not, where I have to do it before controller’s action method called.?
I dont know what you doing with Aquaire request state, Ideally you have to do as below:
[Authorize(Roles="Admin")]
[Route("user/{id:guid}/roles")]
[HttpPut]
public async Task<IHttpActionResult> AssignRolesToUser([FromUri] string id, [FromBody] string[] rolesToAssign)
{
var appUser = await this.AppUserManager.FindByIdAsync(id);
if (appUser == null)
{
return NotFound();
}
var currentRoles = await this.AppUserManager.GetRolesAsync(appUser.Id);
var rolesNotExists = rolesToAssign.Except(this.AppRoleManager.Roles.Select(x => x.Name)).ToArray();
if (rolesNotExists.Count() > 0) {
ModelState.AddModelError("", string.Format("Roles '{0}' does not exixts in the system", string.Join(",", rolesNotExists)));
return BadRequest(ModelState);
}
IdentityResult removeResult = await this.AppUserManager.RemoveFromRolesAsync(appUser.Id, currentRoles.ToArray());
if (!removeResult.Succeeded)
{
ModelState.AddModelError("", "Failed to remove user roles");
return BadRequest(ModelState);
}
IdentityResult addResult = await this.AppUserManager.AddToRolesAsync(appUser.Id, rolesToAssign);
if (!addResult.Succeeded)
{
ModelState.AddModelError("", "Failed to add user roles");
return BadRequest(ModelState);
}
return Ok();
}
Source read here

Calling PasswordSignIn and SendTwoFactorCode in the same request

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

Resources