locked
How to implement user activation RRS feed

  • Question

  • User100248066 posted

    The requirements are simple, but none standard:  Admin creates the user (email, full name, phone) and the user is emailed a link to set the username and password.  I am struggling with implementing the username/password page. 

    * The admin code successfully generates the key
    * The NewUserController.SetupCallback(string code, string username) executes correctly and the page is displayed allowing the user to change the username and set the password

    When clicking on the <submit> button, the screen goes blank and NewUserController.SetupCallback(SetupCallbackModel model) is NOT called.

    I am pondering:  is the SignInAsync correctly signing the user?  I am no pro at working with controls/view and routing in the ASP.NET MVC (framework or core), so there might very well be issues there, too.  Also, if there are other issues folks see and want to point out, I am very open to it all!

    Admin code to create the user:

    var newUser = new ApplicationUser
    {
        UserName = createUser.UserName,
        Email = createUser.Email,
        FullName = createUser.FullName,
        PhoneNumber = createUser.PhoneNumber
    };
    
    var identityResult = await _userManager.CreateAsync(newUser);
    
    if (identityResult.Succeeded)
    {
        var code = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
    
        var url = Url.Action("SetupCallback", "NewUser", new { code = code, username = newUser.NormalizedUserName }, Request.Scheme);
    
        Debug.WriteLine("\n****** --> " + url + "\n");
    
    }
    

    Here is the NewUserController code:

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> SetupCallback(string code, string username)
    {
        var user = await _userManager.FindByNameAsync(username);
        var isValid = await _userManager.ConfirmEmailAsync(user, code);
        //var isValid = await _userManager.VerifyUserTokenAsync(user, "PasswordlessLoginTotpProvider", "passwordless-auth", token);
    
        if (isValid.Succeeded)
        {
            await _userManager.UpdateSecurityStampAsync(user);
    
            await HttpContext.SignInAsync(
                IdentityConstants.ApplicationScheme
                , new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { new Claim("sub", user.Id) },
                    IdentityConstants.ApplicationScheme)));
    
            return View("SetupCallback", new SetupCallbackModel { Username = user.UserName });
        }
    
        return View("Error");
    }
    
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SetupCallback(SetupCallbackModel model)
    {
        if (!ModelState.IsValid)
        {
            model.Password = String.Empty;
            model.ConfirmPassword = String.Empty;
            return View(model);
        }
    
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }
    
        if (!string.IsNullOrEmpty(user.PasswordHash))
            return Redirect(Url.Content("~/"));
    
        if (await _userMgrService.UpdateUsername(user, model.Username) == false)
        {
            return View("SetupCallback", new SetupCallbackModel { StatusMessage = $"Username {model.Username} is already in use." });
        }
    
        var addPasswordResult = await _userManager.AddPasswordAsync(user, model.Password);
        if (!addPasswordResult.Succeeded)
        {
            foreach (var error in addPasswordResult.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
    
            return View();
        }
    
        await _signInManager.RefreshSignInAsync(user);
    
        return RedirectToAction("SetupCallback", new SetupCallbackModel { StatusMessage = "Your password has been set." });
    }
    
    [AcceptVerbs("GET", "POST")]
    public async Task<IActionResult> VerifyUsername(string username)
    {
        var user = await _userManager.GetUserAsync(User);
    
        if (user == null)
        {
            return Json($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }
    
        if (await _userMgrService.VerifyUsername(_userManager.GetUserId(User), username) == false)
        {
            return Json($"Username {username} is already in use.");
        }
    
        return Json(true);
    }

    And for good luck, here is the model used and then the view:

    public class SetupCallbackModel
    {
        [TempData]
        public string StatusMessage { get; set; }
    
        [Required]
        [DataType(DataType.Text)]
        [StringLength(256, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [Remote(action: "VerifyUsername", controller: "NewUser")]
        [Display(Name = "Username")]
        public string Username { get; set; }
    
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string Password { get; set; }
    
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("Password", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
    @model FacilityManager.Models.SetupCallbackModel
    @{
        ViewData["Title"] = "Initial Account Setup";
    }
    
    <h4>Set your username and password</h4>
    <partial name="_StatusMessage" for="StatusMessage" />
    
    <div class="row">
        <div class="col-md-6">
            <form id="set-password-form" asp-action="SetupCallback">
                <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Username" class="control-label"></label>
                    <input asp-for="Username" class="form-control" />
                    <span asp-validation-for="Username" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Password"></label>
                    <input asp-for="Password" class="form-control" />
                    <span asp-validation-for="Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="ConfirmPassword"></label>
                    <input asp-for="ConfirmPassword" class="form-control" />
                    <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
                </div>
                <button type="submit" class="btn btn-primary">Set password</button>
            </form>
        </div>
    </div>
    

    Wednesday, March 17, 2021 6:15 PM

All replies

  • User475983607 posted

    This is a duplicate question. https://forums.asp.net/t/2175035.aspx

    IMHO, you are making a very simple process overly complex.  Meet with your team and come with concrete requirements and application flow.

    I would use a standard username format like an email address, initials and last name, or whatever.  The admin creates an account with a one-time use password.  How you wish to send the one-time use password to the user is up to you.  It could be in an email.  Maybe the username and password are encrypted in a link.  

    Wednesday, March 17, 2021 6:47 PM
  • User100248066 posted

    You are correct on two accounts: 

    1. It is a continuation of the other question.  Difference:  Last time you told me to post source code which I didn't have when I created that other post.  Since I do have source code, I figured I should start a new thread since I have more targeted, meaningful questions.
    2. Yes, it is more complex than the normal way of doing things, but...  I am a developer, I am not the business people making the decisions on how things should be done.  One thing I have learned in many years developing software is that while I can give my opinion and recommendation, at the end of the day it is the business's call on how things work.  Upon bringing up my complexity concerns to my team lead, he 100% agreed with the complexity.  He shared that with the business side and they disregarded it.  He said I was more than welcome to go barking up the tree if I wanted, but at the end of the day, I need to deliver to business what they want.  

    Now that I have done as you asked, written some code, posted it, and asked targeted questions, do you have any insight on my problem?

    Wednesday, March 17, 2021 8:55 PM
  • User475983607 posted

    scarleton

    Now that I have done as you asked, written some code, posted it, and asked targeted questions, do you have any insight on my problem?

    A blank page generally indicates a 500 error.  Something when terribly wrong on the server.  I usually press F12 to look for errors in the network tab. Also the The Visual Studio output window should show any exception that fired on the host.

    The [HttpPost] SetupCallback is configured to allow anonymous but should only allow authenticated users since the [Httpet] SetupCallback signs in the user.  If the user is null then it does not make sense to get the UserId.

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> SetupCallback(SetupCallbackModel model)
    {
        if (!ModelState.IsValid)
        {
            model.Password = String.Empty;
            model.ConfirmPassword = String.Empty;
            return View(model);
        }
    
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

    I'm not sure what userMgrService does.  

        if (await _userMgrService.UpdateUsername(user, model.Username) == false)
        {
            return View("SetupCallback", new SetupCallbackModel { StatusMessage = $"Username {model.Username} is already in use." });
        }

    There are several other logical issues with the code shown.  Go through the code yourself or ask a peer to review the code.

    scarleton

    I am a developer, I am not the business people making the decisions on how things should be done. 

    Yeah, I'm calling shenanigans on that statement.  Yes, business owners provide a flow and requirements but they do not tell you how to design a solution.  I find it hard to believe that the business owner told you login a user without a password using an insecure HTTP GET.   

    Anyway, I would either let the user create the account and an admin approves the account or create a temporary login for the user which the user can use to create an account.  if the user wants to change the username then write a page that allow the user to change the username.  

    Wednesday, March 17, 2021 10:55 PM