Author: Bart Toersche
Almost everyone needs some form of authentication in their front-end applications. We’d prefer to build cool features, but authentication needs to be handled properly which can be a barrier. This blog post shares our thoughts and decisions about handling authentication through OAuth 2.0 using JSON Web Tokens in the Simacan Control Tower.
Simacan Control Tower
The Simacan Control Tower is an application which gives shippers and transporters overview and control on their planning and operation. Its front-end is a single-page web application written in React which receives its data from our micro-service cluster. To do so successfully however, the Simacan Control Tower front-end is required to authenticate its user using a valid JSON Web Token (JWT).
The Simacan Control Tower, like other Simacan products, is part of Simcan’s single-sign-on environment.
Most of the front-ends Simacan builds are written in React and require a proper authentication flow. Designing and implementing the same authentication flow for every single front-end would require a lot of time and effort. Hence a single, pluggable way of handling authentication using a JWT would be very valuable for Simacan.
JSON Web Token
JSON Web Tokens are self-signed authentication tokens, allowing our micro-services to check the validity of these tokens by themselves. This allows keeping the code within each of the micro-services fairly small and doesn’t require requesting a different micro-service for checking whether a token is valid.
The front-end will hold onto a single JWT which is then sent to each of these micro-services. Each micro-service can then successfully authorize access to the requested resources using the provided authentication.
OAuth 2.0 flow
Obtaining a token in the front-end application is done through the OAuth 2.0 authorization-code flow. When opening the application in a browser, it will be redirected to the single-sign-on environment’s login page. After the user logs in, the browser gets redirected back to the application with an authorization code. This code is then exchanged at the authorization service for an access- and refresh token pair.
The access token is a short-lived token which can be used for obtaining private resources from Simacan’s micro-services until it expires. Once the access token expires, a new token pair is requested from the authorization service using the long-lived refresh token.
Since the front-end is a single-page web app, the token can be stored within the React state. So there is no need for a cookie. A session is kept for the SSO login page however. When re-opening the application, instead of grabbing the token from a cookie, a new token is obtained through the login flow using the existing session after which the user will then be able to continue his/her work (assuming the user had already logged in before).
Whenever a user opens our web app without being logged in, no app-related content is shown. Instead a loading screen is shown and the user will be redirected to the login page. An additional benefit of showing a loading screen (other than it looking fancy) is that the actual app isn’t loaded, hence there is no need to worry about the app firing requests before the authentication flow has finished (and therefore receive a lot of 401’s at startup).
This is done by wrapping the app in a login container. The login container handles the auth flow by firing the
handleStartAuthentication action and only renders the actual app once the auth flow has completed successfully (the user is logged in) by returning
index.js, this login container wraps around all of the app-related components.
startAuthentication action fired in the
componentWillReceiveProps performs the actual auth flow. This action is fired multiple times by the login container during the process.
The authentication flow starts in its initial
AUTHSTATUS_NOT_AUTHENTICATED phase. Once the
startAuthentication action is fired for the first time, it will redirect the user to the login page. Once logged in, the user is redirected back to the web app with an authorization code (as redirect URL query parameter).
Note that since the app is reloaded (due to the redirect), the authentication flow phase is reset to its initial
AUTHSTATUS_NOT_AUTHENTICATED phase, but now an authorization code is available.
This authorization code is then used for requesting an access- and refresh token pair from the authorization backend which sets the auth phase to
AUTHSTATUS_IN_PROGRESS. The authorization code is then clipped off of the URL as it has been used (mainly for aesthetic reasons).
If the request returns successfully, the auth phase transitions to
AUTHSTATUS_AUTHENTICATED and the access- and refresh token pair is stored in the state. At this point the app is able to request resources using its access token. However, for displaying the app properly, an extra request is done (using the access token) for obtaining account settings and styling. At this point the auth flow has completely finished and the phase is set to
AUTHSTATUS_COMPLETED after which the actual app is displayed.
Now that an access- and refresh token pair is stored within the state and the actual app is displayed, requests to private resources can be performed using the access token. For requesting resources, a middleware is introduced.
The exact implementation of this middleware is beyond the scope of this blog post. Though roughly speaking, the middleware listens for actions whom represent a request and fires a request for each of these using the fetch API. The response result of these requests are then wrapped in their own action, fired and then picked up by the appropriate reducer.
The middleware will append the request with an Authorization header, containing the access token as its value, if so desired. The authentication is not added to the request by default however. Adding authentication is opt-in, such that accidentally sending an access token to a non-private resource is less likely. This could also be used when using multiple access tokens by instead specifying a key (for instance, realm) such that the middleware would then grab the proper access token from a store using the specified key.
Fetching a private resource
Actions representing a request to a private resource are handled by a
This action attempts to request the resource using the access token. If the resource server declines the authentication, possibly due to the access token being expired, an attempt is done to obtain a new access- and refresh token pair using the refresh token. If this also fails, the user is redirected to the login page where s/he is able to login again.
If a logged in session to the login page is still active however, the user will automatically login again and return to the page s/he came from. The auth flow is performed (obtaining an authorization code and exchanging it for an access- and refresh token pair) and the user can continue using the app again as if nothing had happened.
Once the new access- and refresh token pair is obtained successfully, the initial request to the private resource is retried and its response result is returned.
fetchPrivateResource action in combination with the middleware allows sending authenticated requests with minimal effort (by opting-in). The underlying mechanism handles the request, potential refreshes and retries without developers ever having to worry about authentication. It just happens!
The authentication code can easily be transferred to its own library. For new projects, authentication can then be set up by simply importing the library in
package.json, using the middleware and wrapping the app routes in a
LoginContainer just once and adding
authentication: true to requests wherever required.
Once properly set up, authentication does not need to be a lengthy process. Developers building a cool feature should to just that; build the feature. They shouldn’t need to be bothered with authentication every request they add. Having the application handle the whole authentication flow for them would remove a lot of otherwise required effort from developers. Hence speeding up development time.