In this second part we will start adding an endpoint for authentication to our web API.

To do this we need to add a new controller. We will add an empty API controller and call it AccountsController.

We will use dependency injection to add a UserManager, a SignInManager and a ILogger instance.

[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger _logger;

    public AccountsController(
                UserManager<ApplicationUser> userManager,
                SignInManager<ApplicationUser> signInManager,
                ILogger<AccountsController> logger)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _logger = logger;
    }
}

We also create a new model class to pass login information to our authentication method

public class LoginModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}

last but not least we will add out authentication method to the Accounts controller as an HTTP POST method.

We will allow anonymous access to this endpoint of course.

[HttpPost]
[AllowAnonymous]        
[Route("Authenticate")]
public async Task<IActionResult> Authenticate([FromBody] LoginModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return Ok(model);
        }
        if (result.IsLockedOut)
        {
            return BadRequest("User locked out");
        }
        else
        {
            return BadRequest("Invalid User or Password");
        }
    }

    return BadRequest("Invalid User or Password");
}

calling the Authenticate method using HTTP Post with the following data in the body

{
	Email: "admin@localhost",
	Password: "Admin!1234"
}

will return a status 200 and also a application cookie to be used for authentication.

After that we should be able to call our method on the values controller that we decorated with the Authorize keyword.

Now we will add another endpoint to the Accounts controller that will allow us to add new users to the system

This will be another HTTP Post method that will only be allowed for users that are part of the Administrator role.

In some systems you may want to allow anonymous access if users can register themselves in the system.

First we create a model for the registration data

public class RegisterModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { 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 = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

and then we add the Register method to the AccountsController

[HttpPost]
[Authorize(Roles = "Administrator")]  
[Route("Register")]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{

    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        var result = await _userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            return Ok(result);
        }
        else
        {
            return BadRequest(result);
        }
    }

    return BadRequest("Failed");
}

to allow access only by users belonging to the Administrator role we decorate the method with the Authorize decorator and assign the Roles.

After validating the model we create a new user and return either success or the error information

Next lets add an endpoint to allow the user to change his or her password.

[HttpPost]
[Route("ChangePassword")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordModel model)
{

    if (ModelState.IsValid)
    {
        var user = await _userManager.GetUserAsync(HttpContext.User);

        if (user == null)
        {
            return NotFound();
        }

        var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword );
        if (result.Succeeded)
        {
            _logger.LogInformation("User changed password.");

            return Ok(result);
        }
        else
        {
            return BadRequest(result);
        }
    }

    return BadRequest("Failed");
}

this will allow the user to change the password. The old password has to be known to make the change.

But what if an administrator would need to change the password for a user without having access to the old password.

In order to handle that requirement we add another endpoint called ChangePasswordForUser only accessible to users of the role Administrator

[HttpPost]
[Route("ChangePasswordForUser")]
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> ChangePasswordForUser([FromBody] ChangePasswordForUserModel model)
{

    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByNameAsync(model.UserName);
        if (user == null) {
            return NotFound();
        }

        // compute the new hash string
        var newPassword = _userManager.PasswordHasher.HashPassword(user, model.NewPassword);
        user.PasswordHash = newPassword;
        var res = await _userManager.UpdateAsync(user);

        if (res.Succeeded) {
            _logger.LogInformation("Password for User changed.");

            return Ok(res);
        }
        else {
            return BadRequest(res);
        }
        
    }

    return BadRequest("Failed");
}

This concludes part 2 of this series. In Part 3 we will be looking into additional features for the account management.

Michael Salzlechner is the CEO of StarZen Technologies, Inc.

He was part of the Windows Team at Data Access Worldwide that created the DataFlex for Windows Product before joining StarZen Technologies. StarZen Technologies provides consulting services as well as custom Application development and third party products