Securing Single Page Applications (SPAs) with JSON Web Tokens (JWTs)

Photo by FLY:D on Unsplash

Securing Single Page Applications (SPAs) with JSON Web Tokens (JWTs)

A Single Page Application (SPA), or a Single Page Interface, is a web application displayed through a single HTML page. It dynamically renders the current webpage with new data from the web server. Although the web server indicates a backend, modern SPAs do not necessarily require data from the backend to function correctly. The dynamic server updates have been made possible through recent frontend frameworks and libraries like Angular, React, Svelte, and Vue, which employ concepts such as reactivity and virtual DOM reconciliations as in React. As these frontend frameworks and libraries control data collection, update, and retrieval operations before passing them to the backend, the web application is exposed to security risks, especially during authentication and authorization operations. JavaScript Object Notation (JSON) web tokens help place security measures on frontend authentication and authorization operations. Notwithstanding, the process through which these tokens are stored determines whether the app is exposed to malicious users or secure. In this article, you’ll learn how to properly secure SPAs with JSON web tokens (JWT), store and retrieve tokens safely, handle token expiration and refresh, and prevent and mitigate common SPAs’ security risks.

JSON Web Tokens Basics

JWTs are compact url-safe methods of securely transmitting information between two parties—between frontend applications or from the frontend to the backend. Web developers use JWTs to encode high-value data before transmitting them from one point to another. The encoded data, JSON object, is digitally signed by the sender, the only party with the JWT’s private key. Upon the arrival of the JWT at its destination, the receiver validates the token to ensure that it has not been intercepted or tampered with. JWT contains three critical components—the header, payload, and signature. The header is a JSON object that contains information about the JWT algorithm and type. The payload is the encoded object being sent, and the signature, as mentioned earlier, is the digital print of the sender. The signature helps the receiver know if the encoded data arrived safely. The two primary types of JWTs are access tokens and refresh tokens. Access tokens contain every information the application requires to give access to a user’s request. They are usually short-lived and expire before or immediately after another access token is generated. A refresh token is complementary to an access token. As its name implies, it refreshes (generates) new access tokens once or before the current one expires. While there have been several recent arguments concerning the best choice for securing SPAs, the best argument so far is that you can mitigate CSRF and XSS attacks if you properly store your token on the frontend. In addition, JWT is also applicable to native mobile apps and browser clients.

JWT-based Authentication Workflow

When a user logins or signs up to your application via manual input or Google or social media accounts, their email and password are collected and sent to the login/sign-up endpoint. Once the authentication payload gets to the server, the server responds with a 200 status code and a JSON web token if all goes well. Your SPA then stores the generated JWT in any browser storage API to be accessed in the future for authorization purposes. While the JWT contains a signature to inform you if the data has been tampered with, it does not prevent an attacker from getting your token in transit and accessing its content. JWTs are encoded, not encrypted. However, because the attacker has no access to the secret key held on the server, the attacker cannot change its content and maliciously gain access to your application’s protected resources. To ensure that your application is safe even if the attacker manages to social engineer his way into your app’s server and steal the secret key, you’re strongly advised to allow the expiration and regeneration of all the tokens you use in your application. In the JWT payload, there’s an exp property which stores the expiry date of the token. The secret key changes and the current token is rendered invalid at the appointed time. The backend engineer can also manually invalidate the token without referencing the exp timestamp on the payload by changing the secret key. Besides serving as proof of successful authentication, JWTs also help with authorization. Whenever a logged-in user wants to access a protected route or resources, the client retrieves the JWT from the browser storage API and passes it in the Authorization header of the API call.

Authorization: Bearer <token>

The server checks for a valid token in the authorization header during a user’s request to protected resources and if present, grants the user access.

Secure Storage of JWTs in SPAs

As stated earlier, JWT payloads are not encrypted and are accessible to a 3rd party. How can a 3rd party access a JWT on your user’s browser? To answer that, you’ll need to explore the available storage options for JWTs.

  • Cookies: Cookies are small data packs sent to your browser by your web application when a user visits. It helps to persist stateful information and the user’s browser activities on the browser, thereby increasing load time whenever the user revisits your web app. Web cookies exist in many forms but the two related to this topic are authentication cookies and HttpOnly cookies. Authentication cookies are a special type of cookie used by web servers to authenticate that a user is logged in and the type of account the user used to log in. HttpOnly cookies, on the other hand, store JWT and are only sent in HTTP requests to the server application directly. Together with secure flags, HttpOnly cookies are regarded as the most protected form of storing JWT because they prevent malicious scripts from accessing your tokens and are never accessible through JavaScript running in the browser, making them immune to Cross-site Scripting (XSS) and Cross-site Request Forgery (CSRF) attacks.
  • Web Storage APIs: Web storage APIs are objects that allow you to store key-value pairs in your browser other than cookies. The two main storage APIs are sessionStorage and localStorage.

    • sessionStorage: As its name implies, it stores key-value pairs in the browser as long as a browsing session lasts. Once you close your browser or the tab, all the stored items are deleted. It has a max storage capacity of 5MB.
    • localStorage: This storage API is similar to sessionStorage and has similar operation mechanisms. However, unlike sessionStorage, localStorage data persists even when you close your tab or browser. To clear a localStorage object, you’ll need a JS script, or you can manually clear your browser’s cache or the localStorage API object in your browser’s developer console. Both web storage APIs are accessible via:

      //for localStorage
      windows.localStorage
      //for sessionStorage
      windows.sessionStorage
      //to set an item
      windows.localStorage.setItem("item-key", "item-value");
      //to get an item
      windows.localStorage.getItem("item-key");
      //to remove an item
      windows.localStorage.removeItem("item-key");
      //to clear the storage API object
      windows.localStorage.clear();
      
      //NB: replace `localStorage` with `sessionStorage` to work with the sessionStorage API
      

      Using this code above, you can set, get, remove, and clear tokens from your browser’s storage API.

The con of using these APIs to store JWTs is that they are vulnerable to XSS and CSRF attacks. An attacker can maliciously access your gain access to your token by writing or sending a malicious script or request, respectively. Your storage choice for JWTs in your Single Page Applications should depend on the type of data your JWT payload will contain. If you’re sending a high-value date in the payload, use HttpOnly cookies. If you want the re-login after closing the browser or tab and the JWT payload data is not secretive, use the sessionStorage API. Finally, if you intend to retain the token for an extended period on your browser and enable a seamless experience on the client’s end, use the localStorage API.

Although other storage APIs provide a seamless experience on the app’s users’ ends, none does it, as well as storing the tokens in the localStorage API. Using the localStorage to store tokens prevents the user from logging in every time the tab closes, which is what the user wants—not to log in every time and start the authentication process all over again.

Handling Token Expiration and Refresh

You’ve been introduced briefly to token expiration earlier in this article, but you’ll go further into the processes surrounding token expiration and refresh in this section. In essence, token expiration aims to protect your app’s users from unauthorized access to their accounts by limiting the token's validity by a specified time. Once the access token your app currently uses expires, two actions come into play depending on how the authentication/authorization operation in the backend is set up. In the first scenario, your app could instantly log out the current user and prompt the user to log in again to get a new access token. Otherwise, a refresh token could immediately replace the expired token. In the latter scenario, your users do not need to log out and log in again, as this operation runs in the background. As interesting as the refresh token sounds, it has its cons. If a refresh token has a too-long lifespan, it could continue to supply the current token bearer with new access tokens once the old one expires, and this current token bearer may be your user’s browser or a malicious user’s machine. To be safe, you can tell your backend engineer to explore options such as OAuth, a software security company that ensures that the refresh token supplies only legitimate users. Also, you can inform your backend engineer to expire the current refresh token after supplying several access tokens and create a new one. In this way, even if the malicious user manages to get the access token continuously supplied by a refresh token, once the primary refresh token expires and a new one is created, the malicious user gets cut off from both tokens.

Common Security Risks and Mitigation Strategies

Despite many security measures to prevent malicious users from accessing your clients’ data, these agents develop many novel tricks to authenticate and access your app’s public and protected resources. To keep up with these agents, it is essential to familiarise yourself with their tricks and equip yourself with industry-standard strategies to mitigate them. This section highlights SPAs’ security risks in storing JWTs and how to mitigate them.

Security Risks

  • Cross-site Scripting (XSS) Attacks: XSS is a web security flaw that allows an attacker to compromise users' interactions with an exposed web application. It enables an attacker to develop scripts that disguise them as valid users and grant them access to the user's data as well as the ability to perform any authorised operation remotely. The attacker remotely injects a malicious script into the user's browser. The script then executes a malicious program in the victim's browser, granting the attacker access to the victim's online apps. Because JWTs occur on the client rather than the server, DOM-based XSS attacks typically target them. XSS attacks have a high success rate in web applications that receive form inputs and execute them directly without any form of validation. If the scripts are sent via forms and executed in the victim’s browser, the attacker gains access to the victim’s developer console and proceeds to retrieve or manipulate the tokens. In many cases, this attack exists in the form of social engineering.
  • Cross-site Request Forgery (CSRF): A CSRF attack manipulates victims (some of your users) into unknowingly sending a malicious request from their browser to your application. A successful CSRF attack occurs when your application is unable to differentiate between a free-will request made by your user and one made without your user's knowledge.A CSRF attack is successful when your application cannot distinguish between a free-will request made by your user and one made without your user's knowledge. The attacker cannot obtain the response to the request in this attack; instead, the attacker targets a functionality that triggers a state change. This modification could be to alter the victim's email or password, or it could be to move funds from the victim's account if it’s a financial application. The attacker can deliver a controlled form to your user to conduct the changes and log in again. This type of login form is known as a CSRF login form. Since the attacker controls the form, the attacker gets the user's login details and uses them to access the user’s application.
  • Token Leakage through Browser History: Token leakage occurs through inadvertent storage of access tokens in insecure browser sections such as the browser history. Most cases of token leakage can be traced to the latter scenario. When such a mistake is made, anyone accessing your application’s user’s computer can access the browser history and get away with the token.
  • Token Interception in Insecure Communication Channels: Insecure communication channels are network connections or protocols that are not encrypted or equipped with security measures to protect the transmitted data. Tokens can be intercepted in transit from such channels through a process called ‘token interception’ or ‘token sniffing’. Also, when a token is transmitted over an unprotected protocol like HTTP, it is plainly transmitted, making it vulnerable to attacks such as packet sniffing or man-in-the-middle.

Mitigation Strategies

  • Content Security Policy: CSP is a browser security standard that aims to detect and mitigate the risks of injection attacks such as XSS. It works by restricting the resources that can be loaded on your application. Injection attacks could come in CSS, JS, image and icon links. CSP scrutinizes these resources to ensure they are not malicious before loading them on your users’ browsers. You can implement it in the frontend of your application through a meta tag:
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';" />
    
    See MDN for other methods of implementing CSP in your applications.
  • HTTPS and Secure Cookies: HTTPS and Secure Cookies play a crucial role in securing access tokens and ensuring the overall security of token-based authentication. HTTPS is an extension of HTTP with an added ‘S’ for security. The ‘S’ indicates an extra layer of encryption and security to the information exchanged between the server and the client. It prevents transmitted data from being intercepted and unauthorized parties from accessing data in transmission. Secure cookies are a type of cookies for storing data on the browser, but with a secure flag, unlike other types of cookies. Secure cookies ensure that your users’ browsers only transmit data over HTTPS connections.

By implementing these measures in your applications, you reduce the risks of data access and loss to malicious users and protect your user’s identities.

Conclusion

Single Page Applications are a terrific way to build frontend applications in this modern era, but if care is not taken, you may expose your users’ personal details to attackers. JSON web tokens help authenticate and authorize your application’s users, but it is insufficient. It could be illegally accessed and used to access your user’s protected information through various attack schemes such as XSS and CSRF attacks and frontend developer mistakes like storing them in the browser history or allowing client-server communications across HTTP protocols. In this article, you’ve learned what a JSON web token is, its three major components and the information they hold, the different ways of storing them on the browser, what to do if it gets in the wrong hand, and how to mitigate risks and attacks aimed at stealing them. Incorporating these newly acquired concepts in your future web applications will help you build safer and more robust applications for your user and reduce your worries about XSS and CSRF attacks. If you are a beginner and do not get the concept of authentication and authorization, see my beginner-friendly article on ‘Authentication vs Authorization’; a more robust article on this topic will be released soon, so watch out.

I'm Dr Prime, a Software Developer and Technical Writer. Contact me on Twitter @Tech Evangelist to engage me on tech-related matters and opportunities.