API Security with JWT Authentication

The .Net Core Identity provides a comprehensive framework to manage API security. This even includes database schema to manage users, roles and other types of data related to security.

In this post I will demonstrate with an example how an API can be secured using .Net Core Identity, JSON Web Tokens (JWT) and claims-based authorizations.

Revisions

  • 2019-08-05 : Added support for refresh and access tokens.
  • 2019-06-16 : Original post with support for simple token authentication.

Why JSON Web Token (JWT) ?

The JWT is an open standard with many advantages as indicated here. The scaleability of JWT is yet another signifiant advantage.

The token based authentication schemes fall into two broad categories.

  • Token by reference
  • Token by value

The first category, token by-reference, represents a key to a record that identify a user. The the record must be read to identify the user.

The JWT belongs to the second category. Here the token is self-contained with the necessary information to authenticate/authorize without a table lookup.

Token by Ref vs By Value

Token by-value pre-empts a database lookup for every API request. Token-by value scales well since there is no latency involved with table lookups. The token is digitally signed and optionally encrypted.

A Cost Benefit Analysis

Not just the many advantages but the drawbacks of JWT must be understood in order to get a full picture. Here I describe what they are and ways to mitigate the risks.

The by-value tokens cannot be easily revoked. Anyone is able to gain access if a token falls into the wrong hands. The system is unable to determine if its an authenticated user or an imposter accessing the system.

A similar situation arise when a user loses a password or a Reference Token. However, unlike these schemes there is no database lookup to determine if the token has been revoked. It is possible to check a token with each request. Although this defeats the purpose of JWT designed to scale without database access.

We can minimize the damage by limiting the life-time of the Access Token. The maximum period an imposer can operate without detection can set by limiting the access token expiry. The long term Refresh Token used to generate the Access Token can be made revokable. This is a good compromise that does not completely diminish the scalability of JWT.

The only time this will become a problem is when a user has misplaced a token. It is possible to minimize the risk further by storing the token within a secure area of the client app itself.

The Share Price Viewer Sample

In order to demonstrate JWT and its capabilities we can design and develop a sample app.

The app is a Share Price Viewer that allows users to query and manage a share price index from a mobile app.

API Security App

The app will have a single module in the backend for frontend (BFF) server to control the API endpoint. The backend will handle front-end requests supported by a database.

The client will be a Xamarin app. The intention of the sample is to demonstrate the following features.

  • The Identity API
  • API Versioning
  • API Call tracing and diagnostics
  • Query result caching
  • Implementing JWT on both client and server
  • Handing token expiry
  • Automatic API end-point discovery
  • Re-attempting failed requests
  • Re-using EF model classes as DTOs and for UI binding
  • Working with disconnected entity models
  • Multi-language message support
  • Complying with Microsoft API standards

Refresh vs Access Tokens

The Refresh Token is a long term token. A Refresh Token is issued when a user authenticates and joins the service. This token is rarely renewed and its only purpose is to refresh the Access Token.

The Access Token is frequently used. Therefore database check for its validity becomes expensive. However, a revocation check can be performed cheaply only when new Access Token is generated. This will block access to lost or stolen tokens.

The lifetime of the Access Token represents the maximum period an imposter can operate without being detected. We can minimize the potential for damage by maintaining a low lifetime.

Sample tokens generated are shown below.

A Refresh Token

API Security Permits Config

The encoded token is on the left. The decoded payload with “claims” is on the right. The sole purpose of the Refresh Token is to generate an access token. Therefore it only needs the information required to identify the user.

The sample above shows the claim sub that stands for subject. This presently has a value of 1 identifying the user. The exp claim represent the time the token expires. These are all standard JWT claims.

An Access Token

API Security Permits Config

This is a short lived Access Token that contain sensitive information such as the user email address.

This token is capable of a opening access to a lot more areas of the API. The token itself can be encrypted if required so that the fields remain hidden to all but the server.

A private claim jwt.datej can be seen in addition to the standard claims. This represents the date the Trader joins the organization as an employee. Only the users who has a jwt.datej claim, in other words a legitimate employee is allowed access to the sensitive areas of the application.

The API (See below) only allow SKilledTraders and TraderManagers access to the business end of the application. Here we provide different levels of access based on jwt.datej claim. All users with al least 1 year of service are deemed a “Skilled Trader”. All traders with at least 5 years of service are deemed “TraderManagers”.

The security policy ensures only the TraderManagers can revoke a token thus block a user from the system.

The steps used the produce the app is shown next.

Step 1 - Deploy the Server

Start with the basic details of the app.

API Security App Config

Using Nester Deploy, configure a 1st tier app with a module to manage the API endpoint. Finally deploy the app and download the DevKit.

Step 2 - Define the Entity Model

The .Net Core Identity database schema is predefined by its architects. The classes can be customized and extended to suite application needs.

The Trader entity derived from User is managed by the Identity framework. The two tables Industry and Share manage application data.

API Security DB

Step 3 - Define the API

The API is grouped into Permits, Traders and Industry. Each group is managed by a controller class.

A policy based access restriction to the API will be adopted. The API endpoints covered by the AllTraders policy can be used by all registered users. The SkilledTraders is only for Traders with one or more years experience. The TraderManagers policy limits the API for traders with 5 or more years experience.

The Permits

A Permit is essentially a record that holds a user and its associated JWT token. A permit is issued to an authenticated user to access to the site. The permit has a time limit and expires. The Permits API provides endpoints to create, read and revoke issued permits. Only a Trader manager is able to revoke a permit in other words, the Refresh Token.

API Security Permits Config

The Traders

The User endpoints provide the functionality to maintain traders. The level of access granted to each trader is determined by years of experience.

API Security Users Config

The Industries

This includes endpoints to manage Industry and Share application data. The Admin users will have full control over this API. However, the ordinary users will only be allowed to read the data.

API Security Users Config

Step 4 - Implementation

The Database

In the example given below we make small changes to the default User entity to suit our requirements. For this sample we have added a new field “Date Joined” representing the date a user joined the organization.

The migration process creates and seeds the tables using the configuration classes below.

The .Net Core Identity Entities

The application entities are configured in the similar manner.

Bootstrap

The various application services are added and configured during startup. The two most important statements here are the AddIdentity and AddJwt that adds authentication and authorization services. The two services are described in detail next.

The AddAuthorization code block maps security policies to the user claims contained in the JWT. The Date Joined claim is used to determine if the SkilledTrader or a TraderManager policy applies to a user.

Token Refresh

The way Access Token expiry is handled is demonstrated here. A Token-Expired Http header is returned when the OnAuthenticationFailed is fired. This will remind to the client app to renew the Access Token.

The client-end token expiry is handled in the following way. If the app is configured to AutoTokenRenew then a new Access Token is requested. The request uses the Refresh Token to make the request.

API Version, Device Info and Caching

The API version currently defaults to 1. New versions can be released as and when the API endpoints are updated. This will make it possible to release updates without breaking older apps in production.

The ApiVersion is specified when a client app establish a connection to the BFF. The BFF redirects the request to the area that deal with the API version.

Furthermore, software/hardware versions and other useful information are sent for tracking down issues that may arise in the backend.

The third parameter specify a file to maintain a cache of data. This is for data that hardly change. The data that require an expensive round-trip to the BFF may be cached on the client.

The doCache parameter above allows you to cache the queried data locally.

API Endpoint Name Resolution

The API endpoint name does not have to be specified. The NesterService works out the endpoint from the entity names.

For example, the query above is for shares OwnedBy the _selectedIndustry. The Industry record with a database key 1 produces the following endpoint.

    GET /api/industries/1/shares

The NesterService uses the class name to build the API path. The default name can be changed with the Cloudname class attribute. For example, Industry class name is overridden with its lowercase name.

    [Cloudname("industry")]
    public class Industry : CloudObject
    {
    }

Any object that implement ICloudObject will be able to determine its cloud endpoint.

Request Fail Recovery

The client device may lose mobile coverage at certain times. A retry mechanism is provided to recover from temporary outages.

The backend.RetryCount = x specify the maximum retry attempts. The backend.RetryBaseIntervalInSecs = y determine a base back-off delay after each attempt. The back-off delay become longer starting from the base as the failed attempts increase.

Disconnected Datasets

By default the EF context tracks the operations on a dataset. When the dataset is saved it works out the differences between the original and the latest. A database INSERT is performed if its new data. A database UPDATE is performed if the data already exist in the database.

Change tracking is enabled on the API call illustrated below. The User object cannot be directly sent to client to be updated. Making changes to the object on the client-end would break change tracking on the BFF. The object is copied to a DTO that transfer the data to the client.

The User class on the BFF is typically different from the client-end class. The class on the server has database semantics whereas on the client end would have UI specific functionality.

Change Tracking with DTO

API Security Users Config

No Tracking without DTO

The data transfer can be made more efficient with disconnected datasets. A disconnected dataset no longer tracks changes thus reduces time taken to process a request. Higher level application logic determines whether its an INSERT or an UPDATE.

A DTO is no longer needed as the same object is used for the data transfer. The number of times the data is copied is greatly reduced.

Furthermore, a disconnected object derived from CloudObjectcan be bound to the UI since it implements INotifyPropertyChanged. The same class is used for database, UI and data transfer operations.

The Share object for example is read from the database and sent to the client device. The updated share object that is returned can be used to update the database directly.

API Security Users Config

The change tracker is turned off in the database context constructor.

        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

Caveat

There is however a caveat with objects that maintain sensitive data such as password hashes. Although a password hash belong to the user, it should not be sent to the client device. It should not be sent because the client device cannot be trusted.

The Trader object for example maintains a password hash. Therefore only other non-sensitive data are sent to the client.

.Net Core Identity

The .Net Core identity has many useful configuration options. The authentication can be made more stringent or lenient with the options provided. The framework provides many of the boilerplate code that save tonnes of time and effort. The sample provided at the bottom of this page will demonstrate the settings in action.

The security policies TraderManagers and SkilledTraders authorize the users to various functions. The following example show how a policy is applied to an API endpoint. Here only Admins are allowed to create new industry records.

    [HttpPost("{industry_id}/shares")]
    [Authorize(Policy = "TraderManagers")] 
    public IActionResult Create(int industry_id, [FromBody] Share share)
    {
        ...
    }

API Message Localization

The messages returned from the server can be localized. The LoginFailed result below can be translated to the user’s language configured in the client app.

For this example we use Sinhala and English on an iOS device.

API language Localization - english

The same message appears in the two languages as follows.

API language Localization - english

Refer to Text.resx and Text.si.resx files for the different language strings.

Step 5 - The Demo

Conclusion

The .Net Core Identity framework with JWT can save a lot of time and effort when implementing API authentication and authorization.

The cloud services and the support libraries provided by Nest.yt takes a step further providing an all inclusive solution.

Further Information