Getting Token Authentication Right in a Stateless Single Page Application

Peter Locke
Lightrail
Published in
12 min readJul 7, 2017

Single page application architecture is becoming more prevalent, yet many established patterns to implement authentication security and user experience have not caught up. Patterns used by traditional web applications do not cross over well, or at all, to a true stateless architecture where there is no server-side web session.

The popularity of JSON Web Tokens (JWTs) is well deserved. This fantastic evolution is a building block for solving these problems. As JWTs provide a simple, cryptographically secure means of exchanging information, they make stateless authentication possible. However a building block is just that: a place to start. In going through all the requirements to produce a truly professional SaaS authentication/authenticated user experience in the Lightrail front-end (a stateless single page app backed by the same RESTful API that is published and exposed to external customers), we found that there were gaps in emerging best practices when it comes to a polished customer experience.

This post discusses in detail the JWT based authentication strategy and architecture we decided on, and the technical and user requirements that led us there. Key areas that are covered:

  • Security versus convenience in authentication approaches
  • Multi-factor (2FA) authentication
  • The essential parts of the JWT payload
  • Providing a professional user experience with elegant session timeout and extension
  • Why we chose to split the JWT over two cookies

A high level understanding of JWTs will be helpful in getting the most out of this post. There is an excellent introduction at jwt.io that compliments this post.

Authentication: the Thankless Layer

Logging into an application is a fundamental, crucial, and yet thankless experience. When it works correctly, most users gloss over its underlying complexity. They enter their username and password. Perhaps a piece of secondary authentication such as a code from a text message, TOTP hardware device, or an authenticator app if they have smartly set such a thing up and the service supports it. It is not part of the core product experience - just an expectation. Try talking to your users about salted hashes; they’ll more likely get hungry than think you’re doing a good job of protecting their data.

Authentication has to be secure but it also has to make sense in context of the application; the authenticated experience for Facebook is not the same as for your online banking site. And it shouldn’t be.

Security and Convenience: the Clashing UX Requirements of Authentication

When designing the authentication user experience, convenience and level of security are often at odds.

More Secure

  • Secondary Authentication Method (2FA)
  • No persistent login ‘remember me’ (closing the tab/browser logs the user out)
  • Inactivity timeout (leaving the tab/window open but not using the site for X amount of time logs the user out)

More convenient for the user

  • Username and password only
  • Remain logged in across browser sessions
  • Remain logged in indefinitely regardless of activity

The Requirements We Chose

Using the Facebook vs. online banking analogy again, Facebook and products like them lean to convenience. They want you in your account, serving you those ads, with as few hurdles as possible.

Your online banking site leans the other way, making darn sure you in fact want to log in and that you don’t stay logged in longer than necessary. We took the same security-centric approach with Lightrail, but with a focus on excellent user experience.

End User Requirements

  • Username and password is supplemented with optional 2FA
  • Users remain logged in only as long as they have their browser open
  • Users only remain logged in only if they have been active within the last 30 minutes.
  • If users are inactive for 30 minutes, they are prompted to enter their password and continue their session.
  • Users get a different post-login user experience depending on their account type and user permissions.

Security / Best Practice Requirements:

While the user won’t see any difference from these, we noted them as technical necessities for our stateless architecture and desired best practices.

  • The front-end authenticates with the RESTful API in the same way as external integrators, meaning via supplying a valid JWT for the user on each request.
  • The server does not maintain any session state between calls. There is no stateful middleware layer between the front-end and API.
  • JavaScript/front-end should never have access to the full JWT
  • Everything HTTPS.

With the requirements in hand, we set out to implement the two fundamental pieces of an authentication system:

  • Logging the user in
  • Keeping them authenticated between requests

Let’s dive into each.

Logging in the user

The majority of the traditional flow of registration and accepting a username and password remains unchanged and that’s good. If you’re implementing your own code to hash/salt a password or verify it, that’s a problem flag as it should not be architecture dependent.

The difference comes after the credentials are validated. In a traditional web architecture a session gets created — let’s see how that happens and what requirements it solves that we’ll need to solve differently:

What We Can Learn From Traditional Sessions

A successful authentication happy path results in the creation of an in-memory object on the server that persists across requests (the ‘session’). The user’s browser is given an HTTPOnly cookie that points to that session.

Figure 1: traditional session creation

For each subsequent request the cookie is sent by the browser. If the session is still present on the server, the user is considered authenticated and the request continues. This flow enables a number of requirements essential to any web application, and that we’ll need for our SPA:

  • The username and password only need to be supplied once and not for every request. No brainer.
  • If the user is inactive and the server does not get a request for that user for a set amount of time, they are no longer authenticated (as the session will be removed server side) and will be asked to re-login.
  • Every time a user is active (meaning makes a request to the server), their session length on the server is renewed.
  • If the user closes their browser, they are no longer logged in (as the session-identifying cookie will be gone).

A stateless implementation

As the above shows, the server-side state provides a lot of the desired functionality from our requirements. So how do we make this work without it? This is where JWTs come in. After successful authentication, instead of creating a server-side session, we’ll give the browser a JWT we’ve created for that user. We’ll do this in the form of setting two cookies, each containing a portion of the JWT.

Figure 2: Trading user credentials for a JWT

By splitting the JWT across two cookies, rather than using a single cookie or having the JWT stored in the browser local storage, we are able to meet our requirements without adding additional logic. Let’s see how:

Keeping A User Authenticated with the Right UX: The Two Cookie JWT Approach

It is important to understand how the JWT achieves authentication. From jwt.io:

Authentication: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token.

By including the cryptographically-guaranteed unmodified JWT on every request from the front-end, which contains identifying information for that user such as their uuid and allowed permissions, we’ve achieved a continually authenticated experience with no server-side state. Slick.

The Cookie Split

JWTs take the following form:

header.payload.signature 

To implement our requirements, we put the header.payload in one cookie, and the signature in another, each with specific cookie attributes.

Cookie 1:

header.payload content with cookie attributes:

  • secure
  • 30 minute expiry

Cookie 2:

signature content with cookie attributes:

  • secure
  • HTTPOnly

Over the following sections we’ll see how this split and these attributes enable the discussed requirements.

The Authentication Transformation Layer

To meet our technical requirement of the front-end and external API integrators using the same Lightrail RESTful API, a thin stateless authentication transformation layer is needed.

For external integrators supplying the JWT in the Authentication header, this layer is a passthrough no-op.

For requests that contain the two cookies (front-end requests), it reconstructs the JWT from the cookie content and places it Authentication header that the API expects before passing along the request. This layer also guards against CSRF (more on this later), and updates the header.payload cookie expiry by setting a new cookie on responses that contained valid JWT cookies.

Figure 3: API and web front-end sharing same backing API

User Roles and the JWT

The concept of different users having different permissions is part of any modern authentication system. For example, a team administrator can add and remove team members, but a standard user cannot.

In our architecture this is accomplished through a roles list contained in the ‘payload’ section of the JWT.

header.
{
//other payload data omitted for readability
"roles": [
"teamAdmin",
"accountManager"
]
}
.signature

The roles (each is mapped to a list of allowed endpoints on the back end) provide two key functions:

  • Calls to the API will fail for any type of request for which the user does not have permission.
  • As the roles are part of the JWT payload and therefore JavaScript readable in the header.payload cookie, the front-end can make user interface decisions based on a user’s roles.

Important reminder: you can never trust anything client side readable not to be modified. It is worth noting that if using a symmetric signing algorithm (the same key creates and validates) for your JWTs, the client side cannot read the roles securely as it cannot verify the signature (you’d never have your secret key available client side).

In our implementation (we use symmetric signing) we determined that this is not a problem as the decisions the client-side is making are non sensitive (such as routing) and it is not exploitable server-side due to JWT cryptography. The user interface would become broken/unpopulated as any change to the JWT header.payload cookie content would make the digital signature invalid and all future API calls fail. Users cannot change their roles or elevate their privileges.

As a side note, JWTs do support asymmetric private/public key signing and verification with an all JavaScript JWT library. A public key can be safely given and exposed client side. I would however urge caution if you feel this client-side signature validation pattern is needed as it may indicate a security risk in your architecture. Secure work (and data) should come from the server.

2FA

Implementing 2FA is reasonably painless thanks to roles in the JWT. When the user logs in and 2FA is required they are given a JWT with a role that only allows them to supply the secondary piece of authentication and not access any of their normal account functionality. The front-end will see the different role and prompt them for their secondary authentication code.

header.
{
//other payload data omitted for readability
"roles": [
"2FA",
]
}
.signature

Upon successful verification of the secondary authentication code, the authentication server will include an updated JWT in the two cookies as part of the success response restoring the user’s normal set of roles. A ‘remember this device’ cookie will also be given so that they are not prompted for 2FA on every subsequent login for that browser for the given expiry period of the cookie (we chose 14 days).

Figure 4: The login process when 2FA is required

Other Important JWT Payload Content

Beyond roles, the JWT payload contains what is necessary to the authentication process and security. Note that some of the keys and value format are defined by RFC 7519. By following the specification the JWTs remain compatible with JWT client libraries.

  • useruuid: fundamental, this ties the token to the particular user that is being authenticated.
  • iat: the date/time of issue.
  • exp: A date/time after which the server will no longer accept the JWT as valid. We chose 1 day. This is longer than any legitimate continued use of the Lightrail web application and a user will get a new JWT on each login. Note also that a side benefit is that we need no token refresh functionality (as the user re-entering their login credentials serves that purpose)
  • jti: giving a token a unique identifier enables optional server-side operations like disabling/blacklisting a token if required.

Preventing CSRF

If you’ve followed everything so far, you’d be rightfully worried about CSRF. The JWT, contained in the two cookies, will be submitted along with every browser request.

So what is stopping a malicious user from reading the API documentation, crafting a URL like https://product.your.com/api/dosomething/private, obfuscating it in a shorturl, and tricking the user to click on it?

The crucial server side piece here is for the authentication layer to check for a specific request header that your JavaScript sets before it reconstructs the JWT from the submitted cookies. You must check this server side or your application will be open to CSRF. This reason this defence pattern works is because of the same-origin policy (SOP) restriction that only JavaScript can be used to add a custom header on Ajax requests, and only within its origin. OWASP describes this in more depth.

Same Site Cookies: a Welcome Improvement to CSRF Protection

As of writing this post, a new type of cookie security attribute called ‘Same Site’ was still in draft. http://www.sjoerdlangkemper.nl/2016/04/14/preventing-csrf-with-samesite-cookie-attribute/ This attribute is specifically designed to prevent CSRF behaviour. SameSite cookies appear to be an ideal additional layer of security for this type of scenario.

Why Not Have the Browser Client Send Back the JWT in the ‘Authorization’ Header Instead of Using Cookies?

This decision is worth mentioning as it is a well documented pattern and we considered it. There are advantages, in that the server-side authentication layer would be simplified to just reading the Authorization header and CSRF would no longer be a factor.

Ultimately, we determined that the additional client side complexity to meet the desired requirements, along with the security considerations in letting the JavaScript have access to the full JWT, outweighed these benefits.

Having the signature in an HTTPOnly cookie means JavaScript never has access to the full JWT. The cookies also contain all information and timeout logic around authentication thereby providing an excellent single source of truth.

Recapping Our Requirements

We’ve seen how the JWT across two cookies meets our technical requirements for secure, stateless authentication. Let’s circle back to our user requirements and the rationale behind each cookie and its attributes:

Username and password is supplemented with optional 2FA

Check. JWT role removing/restoring after login and 2FA verification accomplishes this. The front-end can read the JWT role(s) to make routing and display decisions for the user to prompt for 2FA (or not).

Users remain logged in only as long as they have their browser open

Check. The signature cookie will be removed on browser close, and therefore the user will no longer be authenticated as the browser won’t have the full JWT to send to the server anymore.

Users only remain logged in only if they have been active within the last 30 minutes

Check. Unless the user takes an action (thereby resulting in an API call and a new header.payload cookie with a new expiry), their header.payload cookie will expire and they will be no longer authenticated.

Elegant handling and extension of session timeout

Check. The front-end checks for the existence of the header.payload cookie as it is not HTTPOnly. If it doesn’t exist, yet other working state remains, the user is prompted for their password. The password submission simply repeats the credentials-for-JWT handshake shown in Figure 2. Upon success, a new header.payload cookie is issued and the user continues where they left off.

Figure 5: elegant handling of 30 minutes of inactivity

Users get a different post-login user experience depending on their account type and user permissions

Check. The header.payload cookie and therefore roles can be read by the front-end and it can make routing and display decisions for the user — all with the safety of knowing that if the user attempts to modify their roles all calls to the server will be rejected.

In Conclusion

We’ve detailed a JWT based authentication layer for a stateless single page app that focuses on tight security and also provides an elegant authenticated user experience.

--

--

Peter Locke
Lightrail

Entrepreneur | Senior Technical Executive | Software Engineer — Builder of business focussed engineering teams and technology strategies.