JWT Authentication in WebAPI

While there are many 3rd parties that offer ways to handle your authentication, you often come into the situation, where you need to authenticate a user based off a username and password you store yourself. For this scenario, I am going to go through sending the username and password to your API, and returning a token (JWT), that you will use on all future calls in your API. This token will let the API know that you are authenticated and provide the username or id to know who is making the call.

If you want to see a sample Github repo, take a look at MyWebAPI.

Storing a Password in the Database

Before we start off with how to authenticate, I wanted to briefly mention the very important topic of storing passwords in your database. This is a very high security risk and needs proper attention, if you aren’t using a 3rd party to handle authentication. You may be creating another API call to register an account, where they send their username and password. As a side note, your API must be using HTTPS, anything else is insecure.

In your API, you might have a UserController, that will accept a POST of a User object. This is how your API, might accept a username and password to register.

[Route("api/[controller]")]
public class UserController : Controller
{
    // POST api/values
    [HttpPost]
    public void Post([FromBody]User user)
    {

    }
}
public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

In here you will want to get the username and password and pass it to your data repository. However the important part I am focusing on here is how to properly store your password. You want to hash your password, meaning it is non-reversible. A hash can not be converted back to its original string value. There are many hashing functions such as MD5, SHA1, PBKDF2, Bcrypt and more but one of the most secure is Scrypt. It is highly resistant to GPU attacks unlike the others.

To make things easy, I will use the Scrypt.NET package. This will easily hash a password, and you can store it in your database. But before we do that we need to generate a Salt. A Salt is a randomly generated string value, that you generate for each user, each time they store their password. For simplicities sake, it is easy to create a new GUID as your salt.

var salt = Guid.NewGuid().ToString();

Then you add this to your password.

var saltedPassword = password + salt;

Then you hash this function. Don’t forget to store this salt in your database as well.

ScryptEncoder encoder = new ScryptEncoder();

string hashedPassword = encoder.Encode(saltedPassword);

Now when you want to authenticate a user in the future, get their password, get the salt for the user, then compare the hashes. As you can see, it never gets reversed, you just compare the hashes to see if they match. If they do, then the password and salt were the same and they are authenticated.

ScryptEncoder encoder = new ScryptEncoder();

bool areEquals = encoder.Compare(saltedPassword, hashedPasswordFromDatabase);

If you database ever does get breached in the future, they will have a list of emails, and salted and hashed passwords, that will be very hard (though not impossible) to decipher. Unless you are a government agency, or high profile company, it is then unlikely that the attacker will have the resources to dedicate to reverse the passwords.

Owin Middleware

Now, moving on to how to authenticate and generating a JWT for users of your API. First, add these packages to your project, System.IdentityModel.Tokens.JwtMicrosoft.AspNetCore.Authentication.JwtBearer and Microsoft.IdentityModel.Tokens. We can now create our middleware. Add in the TokenProviderMiddleware.cs.

public class TokenProviderMiddleware
{
 private readonly RequestDelegate _next;
 private readonly TokenProviderOptions _options;

     public TokenProviderMiddleware(
            RequestDelegate next,
            IOptions<TokenProviderOptions> options)
     {
          _next = next;
          _options = options.Value;
     }

     public Task Invoke(HttpContext context)
     {
     if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) // If it doesn't match the path
     {
          return _next(context);
     }

     // POST with Content-Type: application/x-www-form-urlencoded
     if (!context.Request.Method.Equals("POST")
     || !context.Request.HasFormContentType)
     {
          context.Response.StatusCode = 400;
          return context.Response.WriteAsync("Bad request.");
     }

     return GenerateToken(context);
 }

 private async Task GenerateToken(HttpContext context)
 {
     var username = context.Request.Form["username"];
     var password = context.Request.Form["password"];

     var identity = await GetIdentity(username, password);
     if (identity == null)
     {
         context.Response.StatusCode = 400;
         await context.Response.WriteAsync("Invalid username or password.");
         return;
     }

     var now = DateTime.UtcNow;
 
     var claims = new Claim[]
     {
     new Claim(JwtRegisteredClaimNames.Sub, username),
     new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
     new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
     };

     var claimList = claims.ToList();
     claimList.AddRange(identity.Claims);
 
     claims = claimList.ToArray();
 
     var jwt = new JwtSecurityToken(
     issuer: _options.Issuer,
     audience: _options.Audience,
     claims: claims,
     notBefore: now,
     expires: now.Add(_options.Expiration),
     signingCredentials: _options.SigningCredentials);

     var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

     var response = new
     {
     access_token = encodedJwt,
     expires_in = (int)_options.Expiration.TotalSeconds
     };

     // Serialize and return the response
     context.Response.ContentType = "application/json";
     await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
 }

 private async Task<ClaimsIdentity> GetIdentity(string username, string password)
 { 
     var result = await AuthenticateWithDatabase(username, password);

     if (result)
         return new ClaimsIdentity(new System.Security.Principal.GenericIdentity(username, "Token"), new Claim[] { });
     else
         return null; // Credentials are invalid or don't exist
 }

 private async Task<bool> AuthenticateWithDatabase(string username, string password)
 { 
     // Connect to your Data Repository

     // You might want to inject IDataRepository into the constructor, to allow communication with your database.

     return false;
 }
}

Then the TokenProviderOptions.cs

public class TokenProviderOptions
{
    public string Path { get; set; } = "/token";

    public string Issuer { get; set; }

    public string Audience { get; set; }

    public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(1);

    public SigningCredentials SigningCredentials { get; set; }
}

These classes are what generates a JWT token, if they successfully authenticate against your database, you can return a valid access token.

You may notice it uses a few variable from the appsettings.json file. Fill them out as needed.

"Auth": {
 "Audience": "MyCompanyName",
 "Secret": "MySecretKey",
 "Issuer": "https://api.mydomain.com/v1/"
 }

Token Generation

We now need to take our middleware and put it in the Owin pipeline, so that it can handle requests. In your Startup.cs, in the Configure method, before app.UseMvc(), add in the following.

 var secretKey = Configuration["Auth:Secret"];

 // Add JWT generation endpoint:
 var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
 var options = new TokenProviderOptions
 {
      Audience = Configuration["Auth:Audience"],
      Issuer = Configuration["Auth:Issuer"],
      SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
 };

 app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options));

Token Consumption

Finally, we need to intercept requests and determine if they are properly authorized. For this we create the TokenOAuthConsumption function in your Startup.cs

private void TokenOAuthConsumption(IApplicationBuilder app)
{

    var secretKey = Configuration["Auth:Secret"];
    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));

    app.UseJwtBearerAuthentication(new JwtBearerOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        TokenValidationParameters = new TokenValidationParameters
        {            
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,

            // Validate the JWT Issuer (iss) claim
            ValidateIssuer = true,
            ValidIssuer = Configuration["Auth:Issuer"],

            // Validate the JWT Audience (aud) claim
            ValidateAudience = true,
            ValidAudience = Configuration["Auth:Audience"],

            // Validate the token expiry
            ValidateLifetime = true,
   
            // If you want to allow a certain amount of clock drift, set that here:
            ClockSkew = TimeSpan.Zero
        }
     });
 }

Now call this function in the Configure method, above the TokenGeneration code, passing in ther IApplicationBuilder.

// Consumption
TokenOAuthConsumption(app);

Secure Controller

Creating a secured API endpoint is now as easy as adding the [Authorize] attribute above a controller. All methods inside this controller will now require a valid JWT in its Authorization header.

[Authorize]
[Route("api/[controller]")]
public class SecureController : Controller
{
    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

Client

Looking at this from a mobile client, we would need to do this to obtain an access token.

// Sent in x-www-form-urlencoded
POST https://mydomain.com/token username=test&password=test

If successful, it would return an access token, such as below.

{
 "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiY2Q1ZDBmYjctYjRjYi00ODNiLTk1MjYtZDZjZDU4OTc1NDAyIiwiaWF0IjoxNDg5ODk1NDc4LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsIm5iZiI6MTQ4OTg5NTQ3OCwiZXhwIjoxNDg5OTgxODc4LCJpc3MiOiJodHRwczovL2FwaS5teWRvbWFpbi5jb20vdjEvIiwiYXVkIjoiTXlDb21wYW55TmFtZSJ9.35NxRTHCmVCSqFaNMuAQZnIFPXLuC3DbJT5Qodhdh6M",
 "expires_in": 86400
}

Then you can send future requests with an Authentication token. Here we will call SecureController.

GET https://mydomain.com/api/secure

// Include this header, key of Authorization and the access token in the value field. However you must put Bearer and a space before the access token in the value field.
Authorization    Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiY2Q1ZDBmYjctYjRjYi00ODNiLTk1MjYtZDZjZDU4OTc1NDAyIiwiaWF0IjoxNDg5ODk1NDc4LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsIm5iZiI6MTQ4OTg5NTQ3OCwiZXhwIjoxNDg5OTgxODc4LCJpc3MiOiJodHRwczovL2FwaS5teWRvbWFpbi5jb20vdjEvIiwiYXVkIjoiTXlDb21wYW55TmFtZSJ9.35NxRTHCmVCSqFaNMuAQZnIFPXLuC3DbJT5Qodhdh6M

It will return with the data as shown in the controller.

["value1","value2"]

For a mobile client you would use HttpClient to perform these requests. Check out the GitHub repo MyWebAPI, if you want to see a working example.

Microsoft MVP | Xamarin MVP | Xamarin Forms Developer | Melbourne, Australia

Related Posts

2 Comments

  1. Andreas

    Hello Adam,

    Great blog! Is there a specific reason for creating a middleware instead of just creating a simple api controller?

    Also, if I know the secret key, I can modify the token (using https://jwt.io/) and even change the username, and thereby become another user. Is this ok or is it insecure?

    1. Adam Pedley

      The middleware is there to decode your token, and generate a token, before it gets to your API Controller.

      Regarding the secret, yes, if you have the secret key, then your entire system is compromised. Keep it safe, and server side.

Leave A Comment?