December 02, 2020
3 min read

Securing SPAs—Best Practices

Today, a Single Page Application or SPA is one of the most popular ways of designing web sites with high performance and better user experience. However, the mammoth challenge in that design itself is that it runs on the end-user’s browser, so it is inherently not a good place to keep secrets. So, during the design, we need to pay special attention to the security aspects of the application.

OAuth 2.0 and OpenID Connect are the most common protocols used to securely authenticate users for an application and access its protected APIs. There are multiple grant types that can be used to protect SPAs.

Grant Type Selection

Implicit Grant (response_type=token)

This grant type was recommended for native apps and JavaScript apps previously as at the time of the introduction of OAuth 2.0 core specification (RFC6749) there was a browser limitation. With the introduction of CORS support, that limitation is no longer there. Also, as described in the OAuth 2.0 Security Best Current Practice draft specification, the implicit grant is vulnerable to access token leakage and access token replay. So this grant type is no longer recommended for use.

Authorization Code Grant

Authorization code grant is the current recommendation for browser-based applications and as these applications are public clients, PKCE(RFC7636) must be implemented.

Use of PKCE

PKCE introduces three new parameters to the authorization code grant flow, namely "code_verifier", "code_challenge" and “code_challenge_method”. In the improved flow, before making the authorization redirect to the server, the application creates and stores a secret named "code_verifier". Then it hashes “code_verifier” and gets the "code_challenge" from it. This "code_challenge" and “code_challenge_method” are passed along with other authorization request attributes to the authorization server. Authorization stores these attributes for later use.

After receiving the authorization code, the application includes the "code_verifier" attribute in the token request made to the authorization server. The authorization server will verify the "code_verifier" with previously-stored "code_challenge" and “code_challenge_method” before issuing the access token.

An attacker who intercepts the authorization code is unable to redeem it for an access token, as they are not in possession of the "code_verifier" secret. You can learn more about PKCE over here.

Use of state parameter

One of the common questions developers have is, as we use PKCE, do we still need the “state” parameter? The answer to that question is yes; they are for two different purposes. The “state” parameter is used by the client to verify whether the authorization code received is for an authorization request initiated by the client. On the other hand, PKCE is used by the authorization server to verify whether it is the original authorization request initiator who is making the token request.

SPAs need to use the "state" parameter to protect themselves against Cross-Site Request Forgery and authorization code swap attacks and MUST use a unique value for each authorization request.

"form_post" response mode

Depending on the way you design your SPA, there will be a possibility to handle a form post, maybe for the first time when an application is invoked. As an example, you may have an “index.jsp” file which returns the SPA application to the browser, and, as it is a JSP, it can handle form post in that time. If that is the case, “response_mode=form_post” is encouraged to be used in authorization requests over obtaining the authorization code as part of the redirect parameter.

With these improvements, an SPA can securely acquire an access token. Then, the question is do we allow refresh tokens for an SPA?

Refresh Tokens

Refresh tokens provide a way to get a new access token when the access token expires, without the involvement of the end-user. For an SPA, this will help to improve the user experience by avoiding redirections when the access token expires.

But, in SPAs, or browser-based applications, token leakage is possible and a leaked refresh token can bring more harm compared to a leaked access token. Because no additional validation happens compared to confidential clients, where the application credentials are validated again in the refresh token grant. An attacker can continue to use it for a long period of time without detection from the authorization server. So, the usage of refresh tokens needs to be handled as a high-risk option.

If you planned to provide the refresh token grant for the SPA, the following restrictions will provide some additional protection.

  • The authorization server should issue a new refresh token with every access token refresh response. This will help to identify and avoid replay attacks and during detection of such an attack, the authorization server must revoke all tokens issued as it is not possible to identify whether the legitimate user or attacker has the valid access token.
  • Having a limited validity period for a refresh token will help to identify any token leakage sooner.
  • Define a required re-authentication time for refresh tokens. This means after a defined time, the refresh token cannot be used further and end-user re-authentication is required.

Now we have enough confidence about acquiring access tokens and refresh tokens for the SPA. So we can start thinking about storing them securely in the application.

Securely store tokens

In a web browser, there are multiple options to store tokens. You can store them in cookies or you can store them in HTML5 storage(sessionStorage or localStorage) or you can save them in the browser memory. So what is the most secure option?

Cookies will be the least secure option among the three as it is vulnerable to both Cross Site Scripting (XSS) attacks and Cross Site Request Forgery (CSRF) attacks. Then, we have the HTML5 storage, which can be accessed via JavaScript, so it is vulnerable to XSS attacks. Finally, we have a browser memory option, which is a solution built using the Web Workers and it is considered the most secure option.

A Web Workers-based solution is more secure because Web Workers run in another global context that is different from the current window. So, it is not possible to access tokens stored in Web Worker using JavaScript running on the browser.

Along with the release of WSO2 IS 5.11.0, we have released a new js library specially designed to handle authentication, secure token storage, and API invocations for SPAs. This implementation uses Web Workers to handle all mentioned operations and provides a rich and easy-to-use API for developers.

What else can we do to secure both tokens and APIs?

In WSO2 Identity Server 5.11.0, you can find a feature called OAuth 2.0 Token Binding, which can be used by the SPAs to give an extra layer of protection to tokens and API invocations. In binding framework implementation, it supports binding OAuth 2.0 tokens to an external parameter like a cookie, header, etc.

In the new version’s implementation, there are two token binding types based on cookie OOTB called “SSO Session Based” and “Cookie Based”. In that implementation, if you hosted your APIs (or API gateway) in the same host as the authorization server (IdP), then, you can additionally validate the token binding as well for your API invocations. So, a token issued for a particular browser instance is valid only inside that browser instance. That means access tokens alone cannot be used to access protected resources. Also, token request, refresh token request, and revoke token requests are also protected with binding. This also provides the capability to issue new token pairs for each new browser instance. Also, token revocation during logout or in session termination events is also possible.

So, in summary, if we apply the current security best practices during SPA development and if we have a capable identity provider, it is quite possible to have high-performing SPA with an improved user experience, securely.