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.
...
"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:
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<JwtTokenValidationSettings>
(Configuration.GetSection(nameof(JwtTokenValidationSettings)));
services.AddSingleton<IJwtTokenValidationSettings,
JwtTokenValidationSettingsFactory>();
...
JwtTokenValidationSettingsFactory
implements the interface and has the function TokenValidationParameters
.
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:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
var tokenValidationSettings =
app.ApplicationServices.GetService<IJwtTokenValidationSettings>();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = false,
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.
"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).
public void ConfigureServices(IServiceCollection services)
{
...
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:
- Collect credentials (email and password).
- Get JWT from issuer.
- Create user and claims from JWT.
- Add JWT to user claims.
- 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
:
public async Task<Boolean> LoginAsync(String email, String password)
{
var jwtToken = await FetchJwtToken(email, password);
return await Login(jwtToken);
}
private async Task<Boolean> Login(String jwtToken)
{
if (jwtToken.IsNullOrEmpty())
return false;
await LogoutAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(jwtToken,
jwtTokenValidationSettings.CreateTokenValidationParameters(), out var validatedToken);
var identity = principal.Identity as ClaimsIdentity;
var securityToken = tokenHandler.ReadToken(jwtToken) as JwtSecurityToken;
var extraClaims = securityToken.Claims.Where
(c => !identity.Claims.Any(x => x.Type == c.Type)).ToList();
extraClaims.Add(new Claim("jwt", jwtToken));
identity.AddClaims(extraClaims);
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
};
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.
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:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
var authenticationSettings =
app.ApplicationServices.GetService<IAuthenticationSettings>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = authenticationSettings.AuthenticationScheme,
LoginPath = authenticationSettings.LoginPath,
AccessDeniedPath = authenticationSettings.LoginPath,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = RefreshTokenMonitor.ValidateAsync
}
});
...
The fresh hook only checks if refresh is required:
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
var issuedClaim = context.Principal.FindFirst
(c => c.Type == JwtRegisteredClaimNames.Iat)?.Value;
var issuedAt = issuedClaim.ToInt64().ToUnixEpochDate();
var expiresClaim = context.Principal.FindFirst
(c => c.Type == JwtRegisteredClaimNames.Exp)?.Value;
var expiresAt = expiresClaim.ToInt64().ToUnixEpochDate();
var validWindow = (expiresAt - issuedAt).TotalMinutes;
var refreshDateTime = issuedAt.AddMinutes(0.5 * validWindow);
if (DateTime.UtcNow > refreshDateTime)
{
var jwtToken = context.Principal.FindFirst("jwt")?.Value;
var claimPrincipalManager =
context.HttpContext.RequestServices.GetService<IClaimPrincipalManager>();
await claimPrincipalManager.RenewTokenAsync(jwtToken);
}
}
The necessary datetimes are fetched from user claims and were set during login. The ClaimPrincipalManager
handles the actual token renewal:
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.
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"));
});
...
The ClaimPrincipleManager
implements the HasPolicy
function:
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:
@inject System.Security.Claims.IClaimPrincipalManager claimManager
@{
ViewBag.Title = "Employees";
}
...
<table id="table">
<thead>
<tr>
...
@if (claimManager.User.HasClaim("Department", "HR"))
{
<th data-field="Salary" data-sortable="true"
data-halign="right" data-align="right">Salary</th>
}
@if (await claimManager.HasPolicy("HR-Manager Only"))
{
<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:
..
"RestClientSettings": {
"BaseAddress": "http://localhost:50249"
},
..
Settings mapping Interface and interface factory:
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)
{
var result = new HttpClient() { BaseAddress = new Uri(BaseAddress) };
result.DefaultRequestHeaders.Accept.Clear();
result.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
var jwtToken = principal.FindFirst("jwt")?.Value;
result.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwtToken);
return result;
}
}
}
Registration during startup:
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RestClientSettings>(Configuration.GetSection
(nameof(RestClientSettings)));
services.AddTransient<IRestClient, RestClientFactory>();
...
Inject REST client in EmployeeController
:
public class EmployeeController : Controller
{
...
private readonly IRestClient restClient;
public EmployeeController(IRestClient client)
{
restClient = client;
...
And use client in the controller:
[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();
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