I have a problem, I want to make the asynchrnous function to finish first before going to the next one. Why? Because the value of the function is needed to the next line of code.
I tried, await, wait(), RunSynchronous but it is not working.
It still proceeds to execute other lines of code.
This is my code: It is the default Login for ASP.NET MVC5 but what I want to do is change the redirect depending on the user.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
SignInAsync(user, model.RememberMe);
RedirectToLocal(LogInRedirect());
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
I have a custom parameter called UserDepartment and I can only retrieve it after SignInAsync(user, model.RememberMe); finish executing.
This is the code for SignInAsync
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
What should I do to achieve my goal that SignInAsync should finish first before proceeding to other line of code.
EDIT:
So await works, but there is another problem.
SignInAsync is not yet finish but it already return that it is completed. By finish I mean the execution of code, I trace the execution but there are times that AuthenticationManager.SignIn is not yet finish but still return it is
You need to await the result of the SignInAsync call
await SignInAsync(user, model.RememberMe);
...
You internally await the result of the UserManager.CreateIdentityAsync method so that will block until that result returns inside the SignInAsync method. However, without awaiting the call to SignInAsync itself, the code won't block which is why the next line is called immediately.
Related
I have a fairly standard Login action, but I want to change the redirect depending on the user role.
However, there's some sort of race condition going on: HttpContext.User says it yielded no results, causing the admin user to be redirected to the wrong homepage.
How do I 'wait' correctly until the HttpContext.User is available after signing in?
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginDto loginDto)
{
if (!ModelState.IsValid)
{
return View(loginDto);
}
var result = await _signInManager.PasswordSignInAsync(loginDto.Username, loginDto.Password, true, false);
if (!result.Succeeded)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(loginDto);
}
// This check doesn't always work because User = null
if(HttpContext.User.IsInRole(RoleEnum.Administrator.ToString())){
return LocalRedirect(Url.Action("Index", "Home", new { area = "Admin" }));
}
return LocalRedirect(loginDto.ReturnUrl ?? Url.Action("Index", "Home"));
}
User UserManager instead of HttpContext in this scope like below
var userInRole = await _userManager.IsInRoleAsync(user, role);
Is there a way to remove a user from a role after a given timespan? When I try something like the below code, I get a null exception once the Delay continues in sessionExpired()...
public async Task<IActionResult> PurchaseSession(PurchaseSessionViewModel model)
{
var user = await _userManager.GetUserAsync(User);
await _userManager.AddToRoleAsync(user, "Active");
await _signInManager.RefreshSignInAsync(user);
// no await
sessionExpired(user);
return RedirectToAction(nameof(Index));
}
private async void sessionExpired(ApplicationUser user)
{
await Task.Delay(10000);
await _userManager.RemoveFromRoleAsync(user, "Active");
}
Note, I understand why the exception occurs but I'd like to retain this type of role-based authorization since [Authorize(Roles = "Active")] provides the functionality I'm after. Is there another way to do this?
Your problem is that your user variable is local and thus is deleted after your first function ends.
You may use a closure (as a lambda function). It is a block of code which maintains the environment in which it was created, so you can execute it later even if some variables were garbage collected.
EDIT: If you want to know why my previous solution didn't work, it is probably because user was being disposed by Identity at a time or another, so here is another try:
public async Task<IActionResult> PurchaseSession(PurchaseSessionViewModel model)
{
var user = await _userManager.GetUserAsync(User);
await _userManager.AddToRoleAsync(user, "Active");
await _signInManager.RefreshSignInAsync(user);
// We need to store an ID because 'user' may be disposed
var userId = user.Id;
// This create an environment where your local 'userId' variable still
// exists even after your 'PurchaseSession' method ends
Action sessionExpired = async () => {
await Task.Delay(10000);
var activeUser = _userManager.FindById(userId);
await _userManager.RemoveFromRoleAsync(activeUser, "Active");
};
Task.Run(sessionExpired);
return RedirectToAction(nameof(Index));
}
I register a user, receive a token via email which looks like this:
Please confirm your account by clicking here
I click the link and I can see that the ConfirmEmail method in AccountController fires:
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
And that result.Succeeded is true.
Everything appears fine, but when trying to log in after completing this process I get taken to the page telling me my account is locked
Locked out.
This account has been locked out, please try again later.
What couldI be doing wrong? Do I need to manually change the lockout flag in the db? If so, what is the point of the ConfirmEmailAsync method?
ConfirmEmailAsync just sets the EmailConfirmed on the user account record to true. From UserManager (edited for brevity):
public virtual async Task<IdentityResult> ConfirmEmailAsync(TUser user, string token)
{
...
var store = GetEmailStore();
...
await store.SetEmailConfirmedAsync(user, true, CancellationToken);
return await UpdateUserAsync(user);
}
Where GetEmailStore returns the IUserEmailStore (which is implemented by UserStore by default), which sets the flag:
public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
...
user.EmailConfirmed = confirmed;
return Task.CompletedTask;
}
The error you're getting indicated that the LockoutEnabled flag on the user account is true. You can set this to false by calling the SetLockoutEnabledAsync method on the UserManager.
There is also a SupportsUserLockout flag on the UserManager which unlocks accounts by default on creation. In order to set this you will need to create your own UserManager and override this flag to false.
At first, I had challenges getting these to work and after a series of research no success. Finally, I got the root of the problem(s) and fixed them thus sharing my experience. Follow the following process and I am sure it will help.
Step 1
Goto Startup.cs and remove the code below if you have it initialised;
services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
//options.LowercaseQueryStrings = true; //(comment or remove this line)
});
Step 2 For GenerateEmailConfirmationTokenAsync() / ConfirmEmailAsync()
2a. On registering new user for token generation go as thus;
var originalCode = await userManager.GenerateEmailConfirmationTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var confirmationLink = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, token = code }, Request.Scheme);
2b. On receiving confrimationLink for email confirmation, go as thus
var originalCode = HttpUtility.UrlDecode(token);
var result = await userManager.ConfirmEmailAsync(user, originalCode);
if (result.Succeeded)
{
return View(); //this returns login page if successful
}
For GeneratePasswordResetTokenAsync() and ResetPasswordAsync()
a.
var originalCode = await userManager.GeneratePasswordResetTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var passwordResetLink = Url.Action("ResetPassword", "Account",
new { email = model.Email, token = code }, Request.Scheme);
b.
var orginalCode = HttpUtility.UrlDecode(model.Token);
var result = await userManager.ResetPasswordAsync(user, orginalCode, model.Password);
if (result.Succeeded)
{
return View("ResetPasswordConfirmation");
}
When the user session ends, a popup shows up in my application and when the user clicks "Stay in the system", the RefreshTheUserSession action of the Account controller is evoked.
public async Task<ActionResult> RefreshTheUserSession(string UserId)//this will be called from the ajax timeout
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var myuser = await UserManager.FindByIdAsync(UserId);
if (myuser != null)
{
await SignInManager.SignInAsync(myuser, false, false);
}
return null;
}
Here is the AuthenticationManager property of the Account controller that comes by default by MVC application.
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
However, when the RefreshTheUserSession action is executed, I receive the following error:
A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
It recommends me to use await and I am using await. I have no idea why this is happening. Any ideas or leads?
I have the following code from AccountController.cs and I am attempting (at my mananger's instruction) to run a unit test against a portion of the login function that validates the ModelState.
Here is the function:
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
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 View(model);
}
}
Notice how the function uses the new "async" keyword along with the "Task" object.
Now I have setup my test like so...
[Test]
public void Account_ModelStateNotValid_ReturnsCorrectView()
{
//Arrange
AccountController ac = A.Fake<AccountController>();
LoginViewModel model = A.Fake<LoginViewModel>();
//Act
var result = await ac.Login(model, null);
A.CallTo(() => ac.ModelState.IsValid).Returns(false);
//Assert
// Assert.That()
}
Nevermind that I haven't completed the function... the reason I haven't completed it, is because right there at
var result = await ac.Login(model, null);
I get the following error:
Error 31 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
I have verified that references are in order (changing the signature of "Login" to "LoginTest" causes an error with my test code not calling "LoginTest"). I'm just wondering if anyone has come across this problem before, and perhaps can tell me what I'm doing wrong.
Thanks in advance.
Decorate the test metod with async or use .Result on the task from the controller.