Click here to Skip to main content
15,885,278 members
Articles / Security

JWT Security Part 3 - Secure MVC Application

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
4 Sep 2017CPOL6 min read 40.6K   2.1K   23   5
Learn how to create JWT and use with WebApi, REST and MVC all build with .NET Core

Introduction

This is the third and last blog about JWT (JSON Web Token). In the first blog, I explained how you can create a JWT, in the second, we secured the REST service. In this last blog, we secure the web application with JWT and covers these topics:

  • Setup and configure JWT
  • Setup Authentication Cookies
  • Connect to JWT Issuer
  • JWT Storage
  • Authentication Cookies
  • ClaimPrincipalManager
  • Sliding Expiration
  • Policies in razor views
  • Accessing REST service

General JWT Security Overview

Before we dive into the details, first a refresher on Part 1 and Part 2. The JWT issuer and the REST service are up and running.

Setup and Configure JWT

The JWT setup and configuration for the website and the REST service is the same. It 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 interface 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:

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

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

JWT is now ready to use.

Get JWT from Issuer

We need connect to the JWT issuer in order to get the token. The connection parameters are set in appsettings.json.

C#
// JWT Issuer
  "JwtTokenIssuerSettings": {
    "BaseAddress": "http://localhost:49842/",
    "Login": "/api/security/login/",
    "RenewToken": "/api/security/renewtoken/"
  },

Login delivers JWT based om user credentials and RenewToken refreshes the expiration window for a valid ticket. I explain this later in more detail. The application reads the configuration with help from DI (Dependency Injection).

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  // Setup JWT Issuer Settings
  services.Configure<JwtTokenIssuerSettings>
           (Configuration.GetSection(nameof(JwtTokenIssuerSettings)));
  services.AddSingleton<IJwtTokenIssuerSettings, JwtTokenIssuerSettingsFactory>();
  ...

The ClaimPrincipalManager uses the setting during a JWT request.

JWT Storage

Web applications are stateless by design. The web application needs some kind of storage for the JWT, otherwise the token must be retrieved every time again for a page request. The application can store the JWT at:

  • HTML5 Web Storage also known as local storage
  • Cookie

Stormpath has a great blog where pros and cons are explained in detail. Web storage has one big disadvantage, the storage is also accessible to others and the web application will have no notion. This makes Cookie storage the preferred option.

Authentication Cookies

It may seem odd but Authentication Cookies handles the security and not JWT. Authentication Cookies handles the authentication, authorization and stores the JWT. Authentication Cookies is hosted in package 'Microsoft.AspNetCore.Authentication.Cookies'. Please view the video tutorial if you want more information about the authentication with cookies.

The login process boils down to:

  1. Collect credentials (email and password).
  2. Get JWT from issuer.
  3. Create user and claims from JWT.
  4. Add JWT to user claims.
  5. Sign in with user and cookie settings .

Step 4, adding the original token to the user claims is not needed for authentication or authorization purposes but gives the opportunity to extract the JWT from the user. The extracted JWT is used for accessing the REST service and sliding expiration. The login is handled by the ClaimPrincipalManager:

C#
public async Task<Boolean> LoginAsync(String email, String password)
{
  // Fetch token from JWT issuer
  var jwtToken = await FetchJwtToken(email, password);

  return await Login(jwtToken);
}

private async Task<Boolean> Login(String jwtToken)
{
  // No use if token is empty
  if (jwtToken.IsNullOrEmpty())
    return false;

  // Logout first
  await LogoutAsync();

  // Setup handler for processing Jwt token
  var tokenHandler = new JwtSecurityTokenHandler();

  // Retrieve principal from Jwt token
  var principal = tokenHandler.ValidateToken(jwtToken, 
  jwtTokenValidationSettings.CreateTokenValidationParameters(), out var validatedToken);

  // Cast needed for accessing claims property
  var identity = principal.Identity as ClaimsIdentity;

  // parse jwt token to get all claims
  var securityToken = tokenHandler.ReadToken(jwtToken) as JwtSecurityToken;

  // Search for missed claims, for example claim 'sub'
  var extraClaims = securityToken.Claims.Where
                    (c => !identity.Claims.Any(x => x.Type == c.Type)).ToList();

  // Adding the original Jwt has 2 benefits:
  //  1) Authenticate REST service calls with original Jwt
  //  2) The original Jwt is available for renewing during sliding expiration
  extraClaims.Add(new Claim("jwt", jwtToken));

  // Merge claims
  identity.AddClaims(extraClaims);

  // Setup authenticates 
  // ExpiresUtc is used in sliding expiration 
  var authenticationProperties = new AuthenticationProperties()
  {
    IssuedUtc = identity.Claims.First
    (c => c.Type == JwtRegisteredClaimNames.Iat)?.Value.ToInt64().ToUnixEpochDate(),
    ExpiresUtc = identity.Claims.First
    (c => c.Type == JwtRegisteredClaimNames.Exp)?.Value.ToInt64().ToUnixEpochDate(),
    IsPersistent = false
  };

  // The actual Login
  await httpContext.Authentication.SignInAsync
  (authenticationSettings.AuthenticationScheme, principal, authenticationProperties);

  return identity.IsAuthenticated;
}

ClaimPrincipalManager

The ClaimPrincipalManager provides easy access to all the security related stuff and implements the IClaimPrincipalManager interface. The interface is registered during startup and an instance is available by the DI (Dependency Injection) pattern. The ClaimPricipalManager is not part of any package, it's only available in the web application.

C#
public interface IClaimPrincipalManager
  {
    String UserName { get; }
    Boolean IsAuthenticated { get; }

    ClaimsPrincipal User { get; }

    Task<Boolean> LoginAsync(String email, String password);
    Task LogoutAsync();
    Task RenewTokenAsync(String jwtToken);

    Task<Boolean> HasPolicy(String policyName);
  }

Sliding Expiration

The JWT expiration is fixed and has no sliding features. When the JWT becomes expired, REST service calls will fail. The Cookie Authentication provides hooks where we can inject the custom code. The algorithm is simple:

  • Check on every page request if the JWT is about to expire.
  • Fetch renewed JWT from the Issuer.
  • Login with the renewed JWT.

The implementation refreshes when the expiration is half way or more. Login with the renewed JWT makes the cookie authentication expiration also sliding and makes sure the user has up to date claims.

The refresh hook is set during startup:

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

  app.UseCookieAuthentication(new CookieAuthenticationOptions
  {
    AuthenticationScheme = authenticationSettings.AuthenticationScheme,
    LoginPath = authenticationSettings.LoginPath,
    AccessDeniedPath = authenticationSettings.LoginPath,
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,

    // Set Refresh hook 
    Events = new CookieAuthenticationEvents
    {
      // Check if JWT needs refreshment 
       OnValidatePrincipal = RefreshTokenMonitor.ValidateAsync
    }
  });
  ...

The fresh hook only checks if refresh is required:

C#
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
  {
    // Find issued datetime
    var issuedClaim = context.Principal.FindFirst
        (c => c.Type == JwtRegisteredClaimNames.Iat)?.Value;
    var issuedAt = issuedClaim.ToInt64().ToUnixEpochDate();

    // Find expiration datetime
    var expiresClaim = context.Principal.FindFirst
        (c => c.Type == JwtRegisteredClaimNames.Exp)?.Value;
    var expiresAt = expiresClaim.ToInt64().ToUnixEpochDate();

    // Calculate how many minutes the token is valid
    var validWindow = (expiresAt - issuedAt).TotalMinutes;

    // Refresh token half way the expiration
    var refreshDateTime = issuedAt.AddMinutes(0.5 * validWindow);

    // Refresh JWT Token if needed
    if (DateTime.UtcNow > refreshDateTime)
    {
      // Get original token from claims
      var jwtToken = context.Principal.FindFirst("jwt")?.Value;

      // Pull ClaimManager from Dependency Injection
      var claimPrincipalManager = 
          context.HttpContext.RequestServices.GetService<IClaimPrincipalManager>();

      // refresh token and claims and expire times
      await claimPrincipalManager.RenewTokenAsync(jwtToken);
    }
  }

The necessary datetimes are fetched from user claims and were set during login. The ClaimPrincipalManager handles the actual token renewal:

C#
public async Task RenewTokenAsync(String jwtToken)
{
  var apiUrl = jwtTokenIssuerSettings.RenewToken;

  using (var httpClient = CreateClient())
  {
    using (var content = new FormUrlEncodedContent
          (new Dictionary<String, String>() { { "", jwtToken } }))
    {
      using (var response = await httpClient.PostAsync(apiUrl, content))
      {
        var renewedToken = await response.Content.ReadAsStringAsync();

        if (response.StatusCode == HttpStatusCode.OK)
          await Login(renewedToken);
      }
    }
  }
}

The token renewal works only when not yet expired JWT. If the token is already expired, the renewal will fail.

Policies in Razor Views

In real world application, the user interface depends on the user permissions. In our web application shows employees. Only the HR (Human Resource) manager is allowed to delete employees. In my previous post, I explained a policy requires one or more claims. The policy is registered during startup.

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

The ClaimPrincipleManager implements the HasPolicy function:

C#
public async Task<Boolean> HasPolicy(String policyName)
{
   return await authorizationService.AuthorizeAsync(this.User, null, policyName);
}

The function relies on interface IAuthorizationService. This interface is available in package 'Microsoft.AspNetCore.Authorization' and is available for DI without explicit registration during startup.

The @inject syntax in a razor view gives returns a IClaimPrincipalManager instance and can be used for policies in a razor view:

JavaScript
@inject System.Security.Claims.IClaimPrincipalManager claimManager

@{
  ViewBag.Title = "Employees";
}
...
<table id="table">
  <thead>
    <tr>
      ...
      @if (claimManager.User.HasClaim("Department", "HR"))
      {
        // Salary only visible to HR department
        <th data-field="Salary" data-sortable="true" 
            data-halign="right" data-align="right">Salary</th>
      }

      @if (await claimManager.HasPolicy("HR-Manager Only"))
      {
        // Delete button only available for HR managers
        <th data-field="" data-formatter="delFormatter" 
            data-visible="true" data-halign="center" data-align="center">Delete</th>
      }
      ...
    </tr>
  </thead>
</table>
...

Accessing REST Service

The web application receives the employees resources from the REST service. The pattern to get it work becomes familiar:

  • Specify settings in appsettings.json
  • Settings class matches the fieldnames in appsettings.json
  • Create Interface for REST client
  • Create Interface Factory
  • Register settings, interface and factory during startup
  • Inject Interface in Controller

REST Client settings:

C#
..
 // REST client
 "RestClientSettings": {
   "BaseAddress": "http://localhost:50249"
 },
..

Settings mapping Interface and interface factory:

C#
namespace System.Config
{
  public class RestClientSettings
  {
    public String BaseAddress { get; set; }
  }
}

namespace System.Net.Http
{
  public interface IRestClient
  {
    String BaseAddress { get; }

    HttpClient CreateClient(ClaimsPrincipal principal);
  }

  public class RestClientFactory : IRestClient
  {
    private readonly RestClientSettings settings;

    public String BaseAddress => settings.BaseAddress;


    public RestClientFactory(IOptions<RestClientSettings> options) : base()
    {
      settings = options.Value;
    }

    public HttpClient CreateClient(ClaimsPrincipal principal)
    {
      // Prepare client
      var result = new HttpClient() { BaseAddress = new Uri(BaseAddress) };

      result.DefaultRequestHeaders.Accept.Clear();
      result.DefaultRequestHeaders.Accept.Add
             (new MediaTypeWithQualityHeaderValue("application/json"));

      // Fetch JWT from user claims
      var jwtToken = principal.FindFirst("jwt")?.Value;

      // Add JWT to header for authentication and authorization
      result.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwtToken);

      return result;
    }
  }
}

Registration during startup:

C#
public void ConfigureServices(IServiceCollection services)
{
  ...
  // Setup REST client
  services.Configure<RestClientSettings>(Configuration.GetSection
                                        (nameof(RestClientSettings)));
  services.AddTransient<IRestClient, RestClientFactory>();
  ...

Inject REST client in EmployeeController:

C#
public class EmployeeController : Controller
{
  ...
  private readonly IRestClient restClient;
  
  public EmployeeController(IRestClient client)
  {
    restClient = client;
    ...

And use client in the controller:

C#
[HttpPost]
[Authorize(Policy = "HR-Manager Only")]
public async Task<IActionResult> Delete(Int32 id)
{
  String url = apiUrl + $"{id}";

  using (var client = restClient.CreateClient(User))
  {
    using (var response = await client.DeleteAsync(url))
    {
      var responseDocument = await response.Content.ReadAsStringAsync();

      // create only response if something off has happened
      if (response.StatusCode != HttpStatusCode.OK)
      {
         var result = JsonConvert.DeserializeObject<ResourceResult<EmployeeResource>>
                      (responseDocument);

         return StatusCode(response.StatusCode.ToInt32(), result);
       }

      return Content(null);
    }
  }
}
...

How the Client Works

The User is passed a parameter in CreateClient(...). The user has the JWT as private claim available and is added to DefaultRequestHeaders and the REST client can now be authorized on the REST server.

Application Demo

Thank you for getting this far. It was a lot to cover and now it's demonstration time.

Start screen:

Click on 'Employees' redirects to login page. Login with (password = password)

  • employee@xyz.com
  • hrwoker@xyz.com
  • hrmanager@xyz.com

Login with employee@xyz.com

Login with hrmanager@xyz.com

As you can see, the user interface depends on the user.

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.

Conclusion

JWT combined with authorization cookies secures web applications and REST services and still offers SSO (Single Sign On) for the user. JWT is self contained, scalable and platform independent.

Previous post: JWT Security Part 2 - Secure REST service

Further Reading

Versions

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

License

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


Written By
Technical Lead
Netherlands Netherlands
I graduated as Bachelor of Mechanical Engineering. Soon I moved from mechanical to software engineering. With more than 20 years of experience in software design, development, and architecture I love building software that users enjoy en suit their needs.

Comments and Discussions

 
QuestionProgramming is an art. Pin
Stefan G - 974211814-May-20 1:01
Stefan G - 974211814-May-20 1:01 
QuestionHow to handle ajax request in MVC project Pin
syedaliaizazuddin24-Apr-19 19:54
syedaliaizazuddin24-Apr-19 19:54 
Questionpart 4 Secure SPA application Pin
tlang331-Sep-17 16:15
tlang331-Sep-17 16:15 
AnswerRe: part 4 Secure SPA application Pin
Bart-Jan Brouwer1-Sep-17 23:25
Bart-Jan Brouwer1-Sep-17 23:25 
GeneralRe: part 4 Secure SPA application Pin
tlang333-Sep-17 6:30
tlang333-Sep-17 6:30 
thats ashame. because i would imagine one of the reasons for using JWT is for SPA and mobile apps. Has anyone done this? i mean, without using AuthO. some guidance would be appreciated by myself and i would imagine thousands of others.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.