Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security

JWT Security Part 2, Secure REST Service

5.00/5 (8 votes)
4 Sep 2017CPOL5 min read 22.2K   699  
Learn how to create JWT and use with WebApi, REST and MVC All Build with .NET Core

Introduction

This is the second blog from three blogs about JWT (JSON Web Token). In the first blog, I explained how you can create a JWT. This blog, we secure the REST service and tackle these topics:

  • Setup and configure JWT
  • Secure by default
  • Setup Swagger for JWT
  • HTTP client with JWT
  • Claim based access
  • Claim based content

General JWT Solution Overview

Before we dive into the details, first a refresher on part one. The JWT issuer is now up and running. It delivers a JWT based on user credentials.

Setup and Configure JWT

The next step is securing the REST service and starts with adding the Microsoft.AspNetCore.Authentication.JwtBearer package. The JWT package needs configuring in startup.cs. First, we set the parameters in 'appsettings.json'.

C#
...
"JwtTokenValidationSettings": {
  "ValidIssuer": "JwtServer",
  "ValidateIssuer": true,
  "SecretKey": "@everone:KeepitSecret!"
},
...

The SecretKey value must match the key in the JWT issuer server, otherwise the user will remain unauthenticated and access will be denied. DI (Dependency Injection) delivers access to the configuration:

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  // setup JWT Token validation
  services.Configure<JwtTokenValidationSettings>
  (Configuration.GetSection(nameof(JwtTokenValidationSettings)));
  services.AddSingleton<IJwtTokenValidationSettings, 
  JwtTokenValidationSettingsFactory>();
  ...

JwtTokenValidationSettingsFactory implements the interfaces and has the function TokenValidationParameters.

C#
public class JwtTokenValidationSettings
  {
    public String ValidIssuer { get; set; }
    public Boolean ValidateIssuer { get; set; }

    public String ValidAudience { get; set; }
    public Boolean ValidateAudience { get; set; }

    public String SecretKey { get; set; }
  }

  public interface IJwtTokenValidationSettings
  {
    String ValidIssuer { get; }
    Boolean ValidateIssuer { get; }

    String ValidAudience { get; }
    Boolean ValidateAudience { get; }

    String SecretKey { get; }

    TokenValidationParameters CreateTokenValidationParameters();
  }

  public class JwtTokenValidationSettingsFactory : IJwtTokenValidationSettings
  {
    private readonly JwtTokenValidationSettings settings;

    public String ValidIssuer => settings.ValidIssuer;
    public Boolean ValidateIssuer => settings.ValidateIssuer;
    public String ValidAudience => settings.ValidAudience;
    public Boolean ValidateAudience => settings.ValidateAudience;
    public String SecretKey => settings.SecretKey;

    public JwtTokenValidationSettingsFactory
           (IOptions<JwtTokenValidationSettings> options)
    {
      settings = options.Value;
    }

    public TokenValidationParameters CreateTokenValidationParameters()
    {
      var result = new TokenValidationParameters
      {
        ValidateIssuer = ValidateIssuer,
        ValidIssuer = ValidIssuer,

        ValidateAudience = ValidateAudience,
        ValidAudience = ValidAudience,

        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)),

        RequireExpirationTime = true,
        ValidateLifetime = true,

        ClockSkew = TimeSpan.Zero
      };

      return result;
    }
  }

The function TokenValidationParameters returns (as the name suggest) JWT validation parameters. These parameters are used during startup.cs.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
   ...
   // Create TokenValidation factory with DI priciple
   var tokenValidationSettings = 
       app.ApplicationServices.GetService<IJwtTokenValidationSettings>();

   // Setup JWT security
   app.UseJwtBearerAuthentication(new JwtBearerOptions
   {
     AutomaticAuthenticate = true,
     AutomaticChallenge = false, // not sure
     TokenValidationParameters = 
          tokenValidationSettings.CreateTokenValidationParameters()
   });
  ...

That's it! Now you can secure controllers or their action with the [Authorize] attribute.

C#
[Authorize]
[Route("api/[controller]")]
public class EmployeeController : Controller
{
  ...

Secure by Default

With MVC framework, you can set custom filters. Securing all controllers by default is just setting a custom filter during startup.

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  // Secure all controllers by default
  var authorizePolicy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();

  // Add Mvc with options
  services.AddMvc(config => 
  { config.Filters.Add(new AuthorizeFilter(authorizePolicy)); });
  ...

Setup Swagger for JWT

The Swagger setup needs a little tweak for JWT support. The tweak allows you to add an JWT during testing. Swagger adds the JWT to the request header and the REST service receives the token and sets the security.

C#
public void ConfigureServices(IServiceCollection services)
{
  ...     
  // Register the Swagger generator with JWT support
   services.AddSwaggerGen(c =>
   {
     // Tweak for JWT support
     c.AddSecurityDefinition("Bearer", new Swashbuckle.AspNetCore.Swagger.ApiKeyScheme()
     {
       Description = "Authorization format : Bearer {token}",
       Name = "Authorization",
       In = "header",
       Type = "apiKey"
      });

     c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info 
                 { Title = "Resources  Api", Version = "v1" });
    });
  ...

The rest of the Swagger setup is normal.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
  ...
  // Enable middleware to serve generated Swagger as a JSON endpoint.
  app.UseSwagger();

  // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), 
  // specifying the Swagger JSON endpoint.
  app.UseSwaggerUI(c =>
  {
    //c.ShowRequestHeaders();
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resources Api v1");
  });
  ...

Time to Test

All is now set to test with swagger. With the small LoginStatus function, you can easily see if the security works.

C#
[Route("api/[controller]")]
public class EmployeeController : Controller
{
  ...
  [HttpGet("loginstatus")]
  public IActionResult LoginStatus()
  {
    var isAuthenticated = this.HttpContext.User.Identities.Any(u => u.IsAuthenticated);
    var email  = this.User.FindFirst(c => c.Type.ContainsEx("email"))?.Value;

   var result = new
    {
      IsAuthenticated = isAuthenticated,
      Email = email
    };
     return Ok(result);
  }

I started Swagger and try the loginstatus.

The response code is 401 meaning 'Unauthorized'. I did not add a JWT, so this is the correct result! Let's check what happens if we provide swagger with a JWT.

First, get a JWT from the JWT issuer with 'employee@xyz.com' and 'password' as credentials.

The swagger value is 'Bearer ' and the response code combined.

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKd3RTZXJ2ZXIiLCJ
zdWIiOiJlbXBsb3llZSIsImVtYWlsIjoiZW1wbG95ZWVAeHl6LmNvbSIsImp0aSI6IjZjNT
QzMzU1LWE4YjItNDI1MC05YTczLTdlZDFmMTNhYzIwZSIsImlhdCI6MTUwNDAxNzc1Miwia
HR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWlt
cy9yb2xlIjoiRW1wbG95ZWUiLCJuYmYiOjE1MDQwMTc3NTIsImV4cCI6MTUwNDAxOTU1Mn0.
0HYDe78_HZ1qVV_uIg5FMqVJujzQC3Pk5dHUX1YvwC0

The JWT is now added to Swagger and we try again:

The response code is 200 (Ok) and the response body shows the authentication is set and the controller can read the claims. Yes!

HTTP Client with JWT

I added a small console application that clearly demonstrates how JWT and a http client can work together.

The first step is retrieving a JWT from the issuer:

private static async Task<String> Login(String email, String password)
    {
      var url = "http://localhost:49842/";
      var apiUrl = "/api/security/login/";

      using (var client = new HttpClient() { BaseAddress = new Uri(url) })
      {
        client.DefaultRequestHeaders.Accept.Add
               (new MediaTypeWithQualityHeaderValue("application/json"));

        var loginResource = new
        {
          Email = email,
          Password = password
        };

        var resourceDocument = JsonConvert.SerializeObject(loginResource);

        using (var content = new StringContent
              (resourceDocument, Encoding.UTF8, "application/json"))
        {
          using (var response = await client.PostAsync(apiUrl, content))
          {
            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
              var result = await response.Content.ReadAsStringAsync();

              return result;
            }

            else return null;
          }
        }
      }
    }

With the JWT, we can make calls to the rest service:

C#
private static async Task<String> GetEmployees(String jwt)
    {
      var url = "http://localhost:50249/";
      var apiUrl = $"/api/employee/";

      using (var client = new HttpClient() { BaseAddress = new Uri(url) })
      {
        client.BaseAddress = new Uri(url);
        client.DefaultRequestHeaders.Accept.Add
               (new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");

        using (var response = await client.GetAsync(apiUrl))
        {
          if (response.StatusCode == System.Net.HttpStatusCode.OK)
            return await response.Content.ReadAsStringAsync();

          else return null;
        }
      }
    }

Please mind the different URLs for login and GetEmployees and watch line 10 carefully. The JWT is added to the request header in the same style as with Swagger, 'Bearer <token>'.

Running the console app:

C#
static void Main(String[] args)
    {
      Console.WriteLine("Request Token");
      var jwt = Login("employee@xyz.com", "password").Result;
      Console.WriteLine($"Token : {jwt}");
      Console.WriteLine("");

      var document = GetEmployees(jwt).Result;
      Console.WriteLine($"Employees: {document}");
      Console.WriteLine("");

      Console.WriteLine($"{Environment.NewLine}Ready, press key to close");
      Console.ReadKey();
    }

results in:

CBAC (Claim Based Access)

The security model supports CBAC and differs from the more traditional RBAC (Role Based Access). I am not starting a discussion is if RBAC or CBAC is better. In think Claim based is more generic and better suited to integrate with different systems. For the RBAC diehards, there is good news, you can perfectly fit a Role based system in a Claim based environment and the Role based support is also added for backward compatibility.

Policy Require Claim(s)

The security model introduces the Policy concept. A Policy is made of one or more claims and in registered during startup.

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  services.AddAuthorization(options =>
      {
        options.AddPolicy("HR Only", policy => policy.RequireRole("HR-Worker"));
        options.AddPolicy("HR-Manager Only", 
        policy => policy.RequireClaim("CeoApproval", "true"));
      });
  ...

A Policy can be fulfilled be either a Role or a Claim. Complex combinations are also possible but they are not used in this demo. You can authorize by role or policy.

C#
[HttpGet("RoleBasedDemo")]
[Authorize(Roles = "HR-Worker")]
public IActionResult RoleBasedDemo()
{
  return Ok("I am role based");
}

[HttpDelete("{id}")]
[Authorize(Policy = "HR-Manager Only")]
public async Task<IActionResult> Delete(Guid id)
{
   ...
 }

However, you cannot use a claim as authorize attribute, you must use a Policy with your Claim.

Claim Based Content

Sometimes, the content of a resource depends on the client. For example, everyone can view employees but only the human resource department is allowed to view the salary. You can implement this feature by checking the user claims or role if you prefer.

C#
private String RemoveSensitiveFields(EmployeeResource resource)
   {
     // The dynamic Linq Select(params) works only on an IQueryable list
     // that's why 1 one item is added to a list
     var items = new List<EmployeeResource>(new[] { resource }).AsQueryable();

     // Find all property names
     var propertyNames = resource.GetType().GetProperties
     (BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).ToList();

     // Salary only visible to HR department
     if (!User.HasClaim("Department", "HR"))
       propertyNames.Remove(nameof(EmployeeResource.Salary));

     // Dynamic Linq supports dynamic selector
     var selector = $"new({String.Join(",", propertyNames)})";

     // Create dynamic object with authorized fields
     var reducedResource = items.Select(selector).First();

     // Create JSON
     var result = JsonConvert.SerializeObject
     (reducedResource, new JsonSerializerSettings()
     { Formatting = Formatting.Indented });

     return result;
   }

Key elements in RemoveSensitiveFields are in line 11 and 18. The function depends on package 'System.Linq.Dynamic.Core'. This package offers a Select(params) extension method on IQueryable items. The property names are extracted with reflection, if the client is not a member of the HR department, the field 'Salary' is removed.

The console application demonstrates the claim based content.

C#
private static void ClaimBasedContentDemo()
{
  // each token represents a different identity
  var tokens = new String[]
  {
    Login("hrworker@xyz.com", "password").Result,
   Login("employee@xyz.com", "password").Result
  };

  foreach (var token in tokens)
  {
    Console.WriteLine(GetLoginStatus(token).Result);
    Console.WriteLine(GetEmployee(token, "jadds4z@1688.com").Result);
    Console.WriteLine("");
  }
}

Output:

As the screen dump shows, only HR members can view the salary.

Conclusion

JWT (JSON Web Tokens) is a solid way for securing REST services. JWT is easy to setup and integrates well with .NET Core. Due to the self contained design keeps JWT the REST service stateless and drops the need for an authentication server. JWT supports both the traditional Role based and the more modern Claim based approach.

Visual Studio Startup Projects

Sometimes, the Visual Studio startup Project is lost and prevents running the application. Right click on the solution and choose 'Set Startup Projects...'

And repair the startup setting:

Choose either WebApp or Jwt.ConsoleDemo depending on what you want.

Further Reading

Versions

  • 1.0 31st August, 2017: Initial release
  • 1.1 5th September, 2017: Source code upgraded for .NET Core 2.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)