Best Practices - OAuth for Single Page Applications
On this page
What is a Single Page Application?
Single-page applications (SPAs) are websites without back ends. Their logic resides in the browser, and they are typically loaded directly from content delivery networks (CDNs).
XMLHttpRequest (XHR) or
Fetch. If the APIs reside in a different domain from the SPAs, the APIs must support Cross-Origin Resource Sharing (CORS) for the browsers to allow the cross-domain communications to take place.
The browser is a hostile place to execute code though, and implementing security is a difficult area of SPA development. This article maps out the necessary measures that need to be taken when securing a Single Page Application using OAuth.
Business Benefits of SPAs
The main motivation for SPAs is to enable a more productive way to build web applications than was possible a decade or so ago. Since SPA backends are only static content, there is no server side logic, and there is a frontend-only focus, as for mobile apps. A modern Single Page Application framework can also be chosen, such as React, Angular or NEXT.js, along with an ecosystem of best practices. The high level benefits are summarized below.
|Great User Experience||All UI actions, including login redirects, are initiated in the browser so that the app has best control over usability|
|Modern Developer Experience||Web developers are freed up to focus on the user interface, and can use the most cutting edge frontend technologies|
|Business Aligned||The improved technical setup should result in more development time being spent building modern screens for customers|
|Web Performance||The use of a CDN can be a simple way to improve web performance, by reducing latency when web resources are downloaded to browsers|
When designing a security architecture for SPAs, you should also ensure that it does not work against any of these goals. This article will explain security best practices for browser based apps, and then recommend an API driven way to implement SPA security, so that the overall architecture is not impacted.
SPAs From an OAuth Perspective
From the OAuth perspective, an SPA exhibits the following:
It must be a public client
Tokens are available in the browser
As tokens are used when communicating with APIs, they are available in the browser. Consequently, they can be obtained by common Open Web Application Security Project (OWASP) defined attacks like Cross-Site Scripting (XSS).
Storage mechanisms are unsafe
It is not possible to store something in the browser safely over a long time without using a back end to secure it. Any browser-based storage mechanism is susceptible to attacks.
Token lifetimes should be kept short
With the before mentioned properties, it stands to reason that any token issued for an SPA should have a lifetime that is as short as possible. The risk of using a longer-lived token needs to be weighed against the potential damage that leakage of such a token can cause.
Backend for Frontend (BFF)
Because of the issues outlined above, the best security recommendation for an SPA is to avoid keeping tokens in the browser at all. This can be achieved with the help of a lightweight back-end component, often described as a
OAuth Agent. This pattern is described in the latest OAuth 2.0 Best Practices for Browser Based Apps.
The backend component can then be configured as a confidential OAuth client and used to keep tokens away from the browser. It can either be stateful and keep tokens in custom storage, or stateless and store the tokens in encrypted HTTP-only, same-site cookies. Whichever variant is chosen, the backend component creates a session for the SPA, using HTTP-only, secure, same-site cookies, thus enabling a high level of security.
Such cookies cannot be read by scripts and are limited to the domain of the SPA. When combined with strict Content Security Policy headers, such architecture can provide a robust protection against stealing tokens. It should be noted, though, that introducing a cookie-based session for the SPA means that it can be vulnerable to Cross-Site Request Forgery attacks (CSRF), and appropriate protections must be put in place.
Since the SPA's OAuth security is now implemented in terms of a confidential client, additional OAuth security standards can be used on behalf of the SPA. See the Financial-grade OAuth Agent for some examples. The Backend for Frontend pattern therefore provides the strongest current security options for an SPA, on par with the most secure websites.
The Token Handler Pattern
At Curity we have designed a modern evolution of a Backend for Frontend solution, which we call the Token Handler Pattern. This keeps web and API concerns separated for the best overall results, and shows how to implement a secure solution without losing any of the benefits of a Single Page Architecture architecture.
For further information, including code examples that can be quickly run in a fully integrated setup on a development computer, see the following detailed resources:
SPA Security Whitepaper
The whitepaper provides a detailed examination of the current state of Single Page Application security, starting with architectures and threats. It then recommends use of either websites or the token handler pattern. The latter is the best all round option, and its usage is explained thoroughly, including end-to-end HTTP messages.
If your setup does not allow to introduce a secure backend component, or for some reason you decide not to do it, have a look at the following best practices, which should be observed in a traditional SPA which handles tokens.
Storing Tokens for SPAs
SPAs can store tokens in the browser in any of the following ways:
This is the least secure option, as it represents longer lived storage across all browser tabs.
This is more secure, since it is restricted to a single tab. Tokens are also removed when the browser tab is closed.
This improves security further, since the tokens cannot easily be intercepted at rest. There are risks that tokens could be intercepted in flight however, either when they are received by the SPA or when they are sent to APIs.
The most secure in-browser storage option is to isolate tokens in a service worker, to reduce the risks that an attacker could exfiltrate them.
Be aware of pros and cons of the chosen storage mechanism
Do not persist tokens between browser restarts or for unnecessarily long time. Be aware of the limitations of storing secure data in the browser.
Using the Code Flow With SPAs
When using the code flow with SPAs the Proof Key for Code Exchange (PKCE) mechanism must be enabled.
The client should not use a secret, since this is public information. When authenticating against the token endpoint the client will use no authentication, and the token endpoint needs to support CORS.
OAuth server configuration for SPAs
As SPAs can't authenticate themselves, the OAuth server is configured to allow the client application to make token requests without authentication.
Using Proof Key for Code Exchange (PKCE)
PKCE is used to prevent common attack vectors against the code flow for public clients. It protects client applications when redeeming tokens as follows:
The client generates a random secret before making the authorization request. The secret hash is passed as the
code_challengeparameter in the request along with the hashing method used.
After receiving the callback, the client adds the plain text secret as the
code_verifierparameter in the request when redeeming the code against the token endpoint.
The token service verifies that the hash of the
code_verifierparameter matches the hash sent in the authorization request earlier, and will only issue tokens if there's a match.
It's recommended to configure the OAuth server to require the PKCE for public clients, so that there are no paths left open where it's possible to do the code flow without the verifier. This is the default in the Curity Identity Server.
Using the Implicit Flow for SPAs
The implicit flow was intended for applications like SPAs in the original OAuth specification. Even though the Best Current Practice (BCP) recommends the code flow with PKCE, there may be scenarios where the implicit flow is relevant. If used, be aware the token is sent in the fragment of the URL back to the client, leaving it visible to any script that can access the history or the current page url.
Tokens are passed in the URI in the implicit flow, which leaves it visible for both users and code running in the application.
Refreshing Access Tokens
Access tokens can be refreshed by either relying on the single sign-on (SSO) session or using refresh tokens.
Using the SSO session
Relying on the SSO session is the recommended approach when the SPA can frame the OAuth server's authorize endpoint securely. SSO sessions are created and represented as secure cookies on the login system after users log in for the first time. The user will not be prompted for a new login as long as the SSO session is alive.
The client simply opens a hidden iframe and starts a new authorization request. The client must pass the parameter
prompt with the value
none to force the server to return an error if the user needs to interact with the server. This means that as long as SSO can be used, the server will return a new
authorization_code that the client can redeem. If the server responds with an error, the client simply aborts the flow and restarts it in a visible window where the user can interact again.
This can also be combined with OpenID Connect session management to avoid any premature redirection and unnecessary network traffic just to check login state.
Note that modern browsers have announced dropping support for third-party cookies. This can render using SSO with hidden iframes impossible, if the Authorization Server does not share the same domain as the SPA. Using designs like the Token Handler Pattern can help mitigate such problems.
Using refresh tokens
Normally when using a refresh token, the client authenticates itself against the token endpoint for the refresh. This prevents the refresh token from being stolen and used by parties that do not possess the secret.
Since the SPA is a public client, it cannot authenticate itself against the token endpoint. It is still allowed to use refresh tokens, but developers should be aware that the refresh token no longer is a protected (or bound) token, and that any party that steals it can use it. For this reason the refresh token, if issued, should have a short lifetime, and must be replaced with a new one upon use.
The default behavior in the Curity Identity Server is to never reuse refresh tokens, and the tokens have a default lifetime of one hour. Public clients created in the admin UI must have refresh tokens enabled explicitly.
Receiving Tokens on the Front Channel
A final word should be written about tokens that are sent on the front channel in the redirect URI. Both the OpenID Connect implicit flow and hybrid flow will pass the ID token in the callback. This means that they are much easier to replace with other tokens by an attacker. For this reason, OpenID Connect defines the
nonce parameter. This is generated by the client and sent in the request to the authorization endpoint. The server will place the same
nonce in the ID token it issues. The client can then verify that the token was indeed issued for that request.
If an access token is received on the front channel alongside the ID Token, an
at_hash value will be present inside the ID Token. The
at_hash is the hash of the access token that was issued at the same time. This prevents the access token from being replaced.
Finally, any ID Token received on the front channel must be validated before usage.
SPAs are different from regular web applications. It is important to be aware of the risks that come with not following the guidelines and protocols properly. This article summarized some topics listed in the Best Current Practices for Single Page Applications, and also recommended use of the Token Handler Pattern for the best overall architecture in a Single Page Application.