Greater control over the user session timeout via spec-defined mechanisms.
Consistency across various use cases (SPA, native mobile apps, native desktop apps, web applications, etc).
I’ve explored these benefits in much greater detail in my previous blog posts. Going forward, the industry is moving toward using the OAuth2 Authorization Code Grant (or OIDC Authorization Code Flow) with a Public Client. In this blog post, we will explore how to best ensure this approach is used with SPAs in a secure manner.
OIDC Authorization Code Flow (with a Public Client) Architecture
To make sure we are all talking about the same thing, let’s assume an architecture and interaction that looks similar to the following diagram.
Furthermore, let’s spell out these assumptions (some, but probably not all, will be true for your use case):
Use a stateless security model rather than a session-tracking cookie with the application backend (API Gateway in the picture above). In this stateless security model, the security context is recreated on the backend for each API invocation using either information in the access token (assume JSON Web Tokens: JWT) or information retrieved from the OIDC UserInfo Endpoint. For more information on this topic, see “How To Submit Your Security Tokens to an API Provider Pt. 1”.
The access token timeout is shorter than the user session timeout. So, the refresh token is needed. The refresh token is being used to extend the life of tokens to satisfy a reasonable session timeout. There are several refresh token strategies. An “offline use” refresh token that is meant to outlive the user authentication isn't appropriate for a SPA. Instead, the idea is that refreshing will fail once the user is required to return to the IdP for interaction (such as reauthentication). The token may not have a set lifetime, but instead fail refresh whenever business intelligence determines user verification is required. The business logic and data behind a refresh token is totally opaque to everything except the IdP; interaction with the IdP is mostly handled by an authentication library except where user interaction (such as providing credentials) is required.
The static asset server will respond to the registered redirect URI with index.html (or equivalent). This way the SPA is reloaded and initialized, and something like Angular Router sees that the Redirect URI has just been called (and takes appropriate steps to complete the OIDC Flow).
This post is not considering the capabilities of any specific Identity Provider vendor. This ensures the information here is applicable to the widest possible audience and that we are focused on the capabilities defined in the specifications.
Now, I am coming at this from a specification-centric vision of the world. This means I am really asking the question, “How do we solve X within the confines of the relevant specs A, B, C, etc.?” Most IdPs provide mechanisms for having control over the length of authenticated sessions, obtaining new tokens without the user being prompted to re-authenticate through a security session tracked with a cookie, and additional functionality that is useful, but proprietary.
So what are the security implications here? The OAuth2, OIDC, and JWT (and supporting) specs provide several mitigating controls to help ensure the integrity of an OIDC login flow, including:
Transport Layer Security (TLS/SSL) Identity Provider (IdP) server certificate is verified by the browser to ensure that the browser is communicating with the real IdP.
Use of a pre-registered Redirect URL. This way it is assured that only the intended application endpoint(s) get access to the tokens.
TLS/SSL static content web server (that hosts the SPA assets) certificate is verified by the browser to ensure that the browser is communicating with the real Redirect URI (and application). This can be taken a step further, beyond the scope of this blog post, and use TLS at every network hop where security tokens are being passed.
Use of the state parameter (and its validation when the authorization code is received) against the Authorization Endpoint, authentication workflow, delegated access consent, and any other redirect endpoints involved.
In accordance with the OAuth2 spec, CSRF protection strategies should be utilized by the IdP’s Authorization Endpoint, authentication workflow, and delegated access consent screens. In fact, it is a good idea to use those on every endpoint generally.
The user will be prompted to give (delegate) consent for the application (SPA) to access their information (profile on the authorization server, IdP). Normally, this would only be done at the first login for each user. To add additional security, this could also be done on every login. This will further ensure that the user is accessing the application they believe they are. Not every OAuth2 Authorization Server supports this.
Although not specifically defined in the specs, the use of Cross-Origin Resource Sharing (CORS) on the Token Endpoint via mechanism that maps valid origins to the described client would provide an additional layer of security that would prevent an impersonating application (loaded from a unregistered origin) from being able to obtain a set of tokens. There are ways to get around CORS restrictions. So, relying upon this by itself is not terribly helpful. Note: other IdP endpoints needed for your use case may or may not support CORS.
Also, not defined in the specs, but store the ID Token, Access Token and (especially) Refresh Token in Session Storage so that it is only available until the browser is closed. This is another topic that has had much debate on the Internet. Unfortunately, there are only a few options for storage in the browser.
Things to Keep in Mind
Now that we’ve looked at some of the security implications of using the OIDC Authorization Code Flow (with a Public Client) for a SPA, here are some things to keep in mind to ensure a better protected system:
Not every IdP supports the OIDC Authorization Code Flow with a Public Client. If your IdP does not, then you will have to rely upon an IdP-specific mechanism (such as session-tracking cookie) to obtain a new access token without prompting for user credentials. It’s been done many times before, but isn’t covered by a spec.
Some IdPs don’t allow CORS on the Token Endpoint. This will prevent the browser from reading the response from the Token Endpoint. So, a SPA with the OIDC Authorization Code Flow and Public Client isn’t supported . If a reverse proxy can be used (legally and technically), that can address any CORS issues with the Token Endpoint. Otherwise, the Implicit Grant (or Implicit Flow) would be the best approach.
Scenarios with a relatively short user timeout could use the OIDC Implicit Flow. If the user’s total session timeout is relatively short and the access token never times out, then a refresh token is not needed. So, using the Implicit Flow is a simplified option.
Do not share the ID Token or Refresh Token with other components of your architecture or third parties. If the SPA is acting as an OAuth2 client (OIDC Relying Party), it shouldn’t pass these tokens to a server-side component for any reason. If your architecture implies this should be done, the server-side component can act as the OAuth2 client and can be a Confidential Client that keeps a client secret protected. Then, the SPA should use a cookie to track the security session with the web server. Sharing the refresh token with any other component of the system compromises the security relationship between the OAuth2 Client (SPA) and the IdP. Sharing an id_token doesn't compromise the security between the IdP and client; it compromises the security of other components that are relying on it (since it was an assertion intended only for that client). You don’t want either of these scenarios to occur.
Dynamic Client Registration (RFC 7591) has some caveats. Dynamic Client Registration (DCR) could add an additional layer of protection for an instance of a SPA application, but the dynamically issued client secret that is issued to an instance of the SPA is yet another piece of information that must be protected alongside the OIDC ID Token, OAuth2 Access Token, and OAuth2 Refresh Token. DCR doesn’t solve the problem of protecting these values in the browser. Your IdP would also need to be able to scale to many OAuth2 clients (one per browser, user) ; imagine your application with millions of users in this scenario. DCR could also provide an effective mechanism for identifying application usage patterns that would allow an IdP to better validate legitimate usage, but that is abstracted away from the application.
Also, keep in mind that all of this assumes that the browser and underlying OS/device have not been compromised in any way. If these are compromised, most of what I’ve mentioned here isn’t going to matter very much.
Historically, the industry has used the OAuth2 Implicit Grant (or OIDC Implicit Flow) with SPAs. There are no security requirements calling for its continued use. The OAuth2 Authorization Code Grant (or OIDC Authorization Code Flow) should be used with SPAs going forward.