Single-page applications (SPAs) are websites without back ends. Their logic resides in 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.
This article maps out the necessary measures that need to be taken when securing a SPA using OAuth.
From the OAuth perspective, an SPA exhibit 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 attacks (XSS) and Cross-Site Request Forgery (CSRF) attacks.
Storage mechanisms are unsafe
It is not possible to store something in the browser safely over a long time without using a backend 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 issue for an SPA should have a time to live that is as short as possible. The risk of using a longer lived token needs to be weighed against the possible damage that leakage of such token can cause.
Tokens can be stored for SPAs in either of the following ways:
The session store
It’s useful, since it’s not persisted before the browser restarts.
The application memory
It’s more volatile compared to the session store.
The local storage
This is not recommended as it is a longer lived storage
Be aware of pros and cons of the chosen storage mechanismDo not persist tokens between browser restarts or for unnecessary long time. Be aware of the limitations of storing secure data in the browser.
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 SPAsAs SPAs can't authentication themselves, the OAuth server is configured to allow the client application to make token requests without authentication.
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 requests. The secret hash is passed as the
code_challengeparameter in the requests 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 the 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.
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 exist scenarios where the implicit flow can be relevant. If used, be aware that 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.
Access tokens can be refreshed by either relying on the single sign-on (SSO) session or using refresh tokens.
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.
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 same 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 the developer 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 explicitly enable refresh tokens for public clients.
In scenarios where the lack of a back end introduce an unacceptable risk for the organization deploying the SPA, using stateless back ends can help mitigate this. With a stateless back end (sometimes referred to as a back-end-for-front-end), the SPA can be turned into confidential client capable of keeping a secret. The back end exposes the callback endpoint for the code flow, and is responsible for exchanging the code against the token endpoint, now capable of authenticating itself. The resulting tokens can then be encrypted and stored in a cookie that only the back end can read. This cookie must be marked as
HttpOnly. The back end end can then either proxy the API calls for the front end, or expose the access token up to the front end.
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 authorize endpoint. The server will place the same
nonce in the ID token it issues. The client can then verify that the token was indeed issues for that request.
If an access token is received on the front channel along side 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 summarize some of the topics lifted in the Best Current Practices for Single Page Applications. Primarily, when using the code flow, always use PKCE.
Let’s Stay in Touch!
Get the latest on identity management, API Security and authentication straight to your inbox.