Application development
.NET Core ASP.NET Core Identity C# JWT REST Swagger Visual Studio Web API

How to Create an Identity Service API using ASP.NET Core

Welcome to today’s post.

Today I will be discussing creating an identity service using ASP.NET Core. 

For a web API identity service we require the following essential methods to be implemented:

  1. Registration
  2. Authentication
  3. Retrieve user list
  4. Lookup any user
  5. Update user account details

Below is the architectural depiction of our service and some of the most common methods used:

Identity service

For registration, we define a basic user view model to hold the minimum user details that are submitted for user registration:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BookLoan.Models
{
    public class UserViewModel 
    {
        public string Id { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime DOB { get; set; }

        public string PhoneNumber { get; set; }
        public string Password { get; set; }
    }
}

All methods of our identity API are implemented as HTTP request methods and documented using Open API Swagger UI.

The Swagger definition for our registration method is shown:

This is a HTTP POST method and the parameter is the request body submitted to the function.

The controller method for user registration is as shown:

[AllowAnonymous]
[HttpPost("register")]
public async Task<IActionResult> Register(
   [FromBody]UserViewModel userDto)
{
   try 
   {
      // save 
      await _userService.Create(userDto, userDto.Password);
      return Ok();
   } 
   catch(AppException ex)
   {
      // return error message if there was an exception
      return BadRequest(new { message = ex.Message });
   }
}

In the registration API method, we do not require any security authorization for the method as the access is anonymous during user registration.

The user manager provider to allow us to create a new user account.

The user service routine creates a new user within the AspNetUsers table:

public async Task<bool> Create(UserViewModel newuser, 
   string password)
{
   var user = new ApplicationUser
   {
      UserName = newuser.Email,
      Email = newuser.Email,
      FirstName = newuser.FirstName,
      LastName = newuser.LastName,
      DOB = newuser.DOB
   };
            
   var result = await _userManager.CreateAsync(user, password);
   if (result.Succeeded)
   {
      _logger.LogInformation("User created a new account with password.");

      var code = await
        _userManager.GenerateEmailConfirmationTokenAsync(user);

       _logger.LogInformation(
"User created a new account with password.");

       // add additional claims for this new user..
       var newClaims = new List<Claim>() {
          new Claim(ClaimTypes.GivenName, user.FirstName),
          new Claim(ClaimTypes.Surname, user.LastName),
          new Claim(ClaimTypes.DateOfBirth, 
             user.DOB.ToShortDateString())
       };
       await _userManager.AddClaimsAsync(user, newClaims);
       _logger.LogInformation("Claims added for user.");

       return true; 
    }
    return false;
}

In our registration process we make use of the ASP.NET Identity API to allow us to maintain users within our data store.

The ASP.NET Identity API allows us the following functions:

  1. Add/update users
  2. Add/update user claims
  3. Add/update user passwords

To create a user and test our registration method we can use a utility such as POSTMAN as shown:

Our API URI for registration is of the form:

http://[server]/api/Users/Register

and our request body contains the submission data is JSON format:

{
  "userName": "andy",
  "email": "[email protected]",
  "firstName": "andy",
  "lastName": "hill",
  "dob": "1989-01-24T00:00:00.000Z",
  "phoneNumber": "12345678",
  "password": "[email protected]"
}

If you have set a breakpoint within the controller then you can view the submitted data in the [FromBody] parameter as shown:

After submission to our API the user data is created in our datastore in the AspNetUsers and AspUserClaims tables.

You will also notice that the password is hashed and stored within the HashPassword column and entries inserted into the AspNetClaims table:

The next method that is significant is for user authentication.

The swagger definition for authentication is shown below:

For the authentication process we will achieve these goals:

  1. Check if the user credentials are valid
  2. Return a unique JWT token.

The controller for authentication is shown below:

[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]UserViewModel userDto)
{
   try
   {
      bool isAuthenticated = 
  _userService.Authenticate(userDto.UserName, userDto.Password);

      if (!isAuthenticated)
         return BadRequest(
            new { message = "Username or password is incorrect" });

      string tokenString = 
         _tokenManager.GenerateToken(userDto.UserName);

       // return basic user info (without password) and token to 
       // store client side
       return Ok(new
       {
          Username = userDto.UserName,
          Token = tokenString
       });
    }
    catch (Exception ex)
    {
       return BadRequest(ex.Message.ToString());
    }
 }

The user service to authenticate is shown:

public bool Authenticate(string username, string password)
{
   if (string.IsNullOrEmpty(username) || 
      string.IsNullOrEmpty(password))
   throw new AppException("The username or password is empty.");

   var user = _db.Users.SingleOrDefault(x => x.UserName == username);

   // check if username exists
   if (user == null)
      throw new AppException("The username is invalid.");

   // check if password is correct          
   PasswordHasher<ApplicationUser> passwordHasher = new  
      PasswordHasher<ApplicationUser>();
   if (passwordHasher.VerifyHashedPassword(user, 
      user.PasswordHash, password) 
      == PasswordVerificationResult.Failed)
   {
      throw new AppException("The password is invalid.");
   }

   // authentication successful
   return true;
}

The above authorization service achieves the following:

  1. Checks the user / password are valid
  2. Checks the user name is a valid user name within our database
  3. Checks the password hash is valid

To verify the password hash we have a helper function that achieves this:

private static bool VerifyPasswordHash(string password, byte[] 
   storedHash, byte[] storedSalt)
{
   if (password == null) throw new ArgumentNullException("password");
   if (string.IsNullOrWhiteSpace(password)) throw new 
      ArgumentException(
"Value cannot be empty or whitespace only string.", "password");
   if (storedHash.Length != 64) 
      throw new ArgumentException(
"Invalid length of password hash and salt (84 bytes expected).", "passwordHash");

   using (var hmac = 
      new System.Security.Cryptography.HMACSHA512(storedSalt))
   {
      var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
      for (int i = 0; i < computedHash.Length; i++)
      {
         if (computedHash[i] != storedHash[i]) return false;
      }
   }

   return true;
}

Note: With ASP.NET Core Identity all passwords are stored as a concatenated 84 character string:

[Salt] + [Hashed Password]

The salt itself is 20 characters, and since the key encryption we use is HMACSHA512, a 512-bit encryption algorithm, the size of the password hashed is (512/8) = 64 bytes.

The code to generate a JWT token is shown below:

public string GenerateToken(string username)
{
   byte[] key = Convert.FromBase64String(appConfiguration.Secret);
   JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();

   SymmetricSecurityKey securityKey = new SymmetricSecurityKey(key);
        
   SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
   {
      Subject = new ClaimsIdentity(new[] {
         new Claim(ClaimTypes.Name, username)}
      ),
      Expires = DateTime.Now.AddMinutes(30),
      SigningCredentials = new SigningCredentials(new 
      SymmetricSecurityKey(key),
      SecurityAlgorithms.HmacSha256Signature)
   };

   JwtSecurityToken token = 
      handler.CreateJwtSecurityToken(descriptor);
   return handler.WriteToken(token);
}

Note: we generate a token using a basic claim consisting a username. In real-world applications the claim will consist of many claims such as email, date of birth, registration id etc.

We have also set an expiry for the token with the SecurityTokenDescriptor expiry property we set the expiry to 30 minutes:

Expires = DateTime.Now.AddMinutes(30),

If we did not set an expiry for the token, then if the token could be reused indefinitely. By placing an expiry time on the token we force end-users to login to regenerate a new token.

To test authentication use POSTMAN

You will need to enter or paste the submission data into the body, select raw format and json format as shown:

Our API URI is of the form:

http://[server]/api/Users/Authentication

and our request body contains the submission data is JSON format:

{
"userName": "[email protected]",
"password": "[email protected]"
}

Should the authentication fail, we will get a 400 bad request and a message in the body:

When the submission succeeds, you should get a response in the body as shown:

In the case that user data is not created, check the bottom of the POSTMAN screen for any errors. If you see an error like the one shown:

Then open the Output pane within Visual Studio to see what caused the error:

Then rectify the issue, rebuild and retest. Common problems with .Net Core Web APIs might be with the configuration of services using dependency injection. One of my earlier posts covers this in some detail. 

In a future post I will show how to use the authentication token to access other APIs that require custom authentication and / or authorization.

That’s all for today’s post.

I hope you found it informative and useful.

Social media & sharing icons powered by UltimatelySocial