Single-Page Applications (SPAs) are fast becoming ubiquitous as they provide a far better user experience over the traditional multi-page applications. However, as is often the case with app development, a good user experience and security don’t see eye to eye. In SPAs too, the merits of SPAs come at the cost of security. But it doesn’t have to be that way. By building SPAs taking into consideration the security limitations of SPAs and by following the best practices, we can mitigate most of the security concerns in SPAs.
This article exclusively focuses on the best practices for using the OpenID Connect (OIDC) protocol for authentication in SPAs. Nonetheless, some of the best practices apply to SPAs irrespective of the authentication protocol used.
For starters, session management is a bare minimum requirement of any SPAs. However, it becomes a huge security challenge in SPAs as client-side storage in the browser is exposed to the outside world.
Most developers choose to store the session information such as access tokens in session or local storage. Even though session storage is comparatively more secure than local storage, neither of them is immune to access by third-party code. This makes SPAs vulnerable to Cross-Site Scripting (XSS) and typosquatting attacks.
One solution is to use http-only cookies. But this makes the app susceptible to Cross-Site Request Forgery (CSRF) attacks. Besides, if your authentication server and API endpoints exist in two different domains, then http-only cookies cannot be used at all.
A common solution is to have a dedicated backend for your SPA, where you can store the session information. The backend will act as a gateway for all your network requests. However, this solution adds a lot of complexity to your development.
A more workable solution is to use web workers to store session information. Web workers are essentially background threads that have a different browsing context. So, third-party codes cannot access information stored in the memory of web workers. This protects the app from both XSS and typosquatting attacks. Asgardeo’s SPA SDKs allow developers to use web worker as a storage option, which greatly improves the security of the apps. The Console and the My Account apps of Asgardeo both use this method to store their session information.
Use HTTPS in SPAs
HTTPS offers a secure channel between the client app and the backend by encrypting communication using Transport Layer Security (TLS). This makes sure the access token used to make requests to protected endpoints is not stolen by attackers. The use of HTTP which doesn’t encrypt communication can leave your SPA vulnerable to man-in-the-middle attacks.
Authorization-code grant over implicit grant
Even though the implicit grant was meant for client-side applications, this has long outlived its purpose. Therefore, it is always advisable to use the authorization-code grant over the implicit grant in your SPAs. There are several reasons for this.
To begin with, the implicit grant type returns the access token in the URL. So, a permanent record of the access token is created in the browser history, which can lead to the access token being compromised easily. Moreover, modern-day browsers store the browser history in the cloud. This can take your access token to the cloud and make it accessible across multiple devices.
The authorization grant is a lot safer since only the authorization code is returned in the URL. There are even ways to mitigate this relatively minor concern and we shall discuss it later in the article. The SPA can then use this code to get an access token. OIDC allows token exchange without the client secret for public clients, so the SPA doesn’t need to store the client secret anywhere either.
Form post over query response mode
OIDC allows two different ways to get the authorization code. One is the popular query mode where the code is returned as a query parameter in the redirected URL. This has similar concerns to the implicit grant as the code is persisted in the browser history.
To mitigate this, you can use the form post mode. When you use the form post mode, the code is sent in the body of a post request to the endpoint specified by the redirect URL. This adds some complexity to your application as you need a backend to capture the authorization code sent. But the enhanced security is worth the effort.
Refresh tokens allow SPAs to obtain a new access token when it expires without requiring user intervention. This provides a better user experience but should the refresh token be compromised it can do more harm than having the access token compromised. This is because OIDC does not require client authentication when issuing a new access token when the refresh token is presented.
This can be mitigated by ensuring that a new refresh token is issued every time the access token is refreshed. This prevents replay attacks. Furthermore, the validity period of the refresh token should be kept short so that the scope of the damage caused by an attack is greatly reduced.
Use PKCE in SPAs
PKCE stands for Proof Key for Code Exchange and is an extension of the authorization-code grant. This allows the OIDC provider to ensure that the party requesting an access token using an authorization code is the same party that requested the authorization code in the first place.
This involves sending a secret key along with the authorization code request and access token request. The OIDC provider will keep a record of the secret key and the authorization code issued. When an access token is requested with an authorization code, the provider checks to see if the secret key attached to the token request is the same as the secret key used to request the authorization code. If they are the same, then the access token is issued.
This prevents a compromised authorization code from being used to maliciously obtain an access token. This provides an extra layer of security in public clients that don’t use a client secret to authenticate themselves with an OIDC provider.
Use the state parameter
The state parameter allows a client to know if the authorization code issued to it is for the authorization request sent by it. When generating an authorization request, the client sends a key as the value of the state query parameter. When the OIDC provider issues an authorization code and redirects the browser to the configured redirect URL, the state parameter is also appended to the URL. By checking if the state parameter value in the redirected URL is the same as the value sent during the authorization code request, the client can ensure that the code received was for the request it had sent.
This is important to prevent code-swap attacks and CSRF attacks.
Token bindings provide more protection to protected API endpoints. You can employ two types of token bindings—cookie-based, and SSO-session-based.
In cookie-based token binding, an http-only cookie is returned with the authorization code. When the token request is sent, this http-only cookie is also sent along with the request. The OIDC provider will then validate this cookie and attach it to the access token. Following this, if the SPA needs to access protected endpoints it needs to produce both the access token and the http-only cookie. This protects the API endpoints even if the access token is leaked or stolen.
In SSO-session-based token binding, different access tokens are generated for each browser instance. This ensures that the same access token is not shared across multiple browser instances of the SPA.
Implement Single Log Out in SPAs
If multiple SPAs are using the same OIDC provider, and a user logs out of one of the SPAs, then the user should be logged out of all the SPAs. Not doing so will allow access to protected endpoints in other SPAs even though the user has ended the user session with the OIDC Provider.
This can be accomplished by using the check session iframe. By querying the check session frame, a SPA can know if the session state has changed. If it has changed, a new authorization code request can be dispatched from within an iframe. If a code is returned, then that means the user has an active session in the OIDC provider. If a code is not returned, then that means the user has signed out. So, the SPA should log the user out from the app.
By following these best security practices when developing a SPA, we can make sure that the better user experience provided by the SPAs doesn’t come at the expense of security. But some of these best practices can consume a lot of development time add a lot of complexity to your project. This is why Asgardeo’s SPA SDKs incorporate most of these best practices so that you can implement these in your SPAs without too much effort. With a competent identity server and a suite of secure SDKs, Asgardeo can make your SPAs safe and secure for use. So, checkout Asgardeo to quickly develop a SPA that doesn’t compromise on security.