A Primer on OAuth 2.0 for Client-Side Applications: Part 2
- Johann Nallathamby
- Director - Solutions Architecture - WSO2
Introduction
This is the second part of a four-part article series. In the first article, I looked at the broader categories of client-side applications (CSA), their legacy and more recent authentication and API authorization standards, and their pros and cons.
In this part, I will discuss the challenges in CSAs, standard and non-standard recommendations that are widely employed to overcome those challenges, and some of the non-standard solution patterns for single-page applications (SPA) to overcome limitations in the existing standards I have discussed in part one, specifically regarding OAuth 2.0 proxy patterns.
Challenges in OAuth 2.0 Public Clients
If client credentials are compromised, the following threats become imminent.
- An illegitimate client can invoke APIs protected with a client credentials grant flow, which it is not authorized to invoke.
- An illegitimate client can perform a denial-of-service attack on the resource server.
- An illegitimate client can impersonate a legitimate client to trick the user, to obtain his/her consent and access resources on behalf of the legitimate user.
If access tokens are compromised, the following threats become imminent:
- An illegitimate client can invoke APIs that it is not authorized to invoke.
- An illegitimate client can eat-out the throttling quota associated with an access token.
If refresh tokens are compromised, the following threats become imminent:
- An illegitimate client can invoke the OAuth 2.0 token endpoint using refresh_token grant flow with the refresh token without even requiring client authentication.
Recommendations for OAuth 2.0 Public Clients
The following recommendations are provided to mitigate threats due to compromised client credentials:
- Reject any access tokens used to access APIs, which are issued under a client credential grant flow.
- The authorization server must require public clients to register their redirect URIs and enforce a strict validation with the redirect_uri parameter in the authorization request.
- Provision per-instance client identifiers for native applications. The OAuth 2.0 Dynamic Client Registration profile (RFC 7591) [1] may be used for this purpose.
- One-time-use client identifiers / rolling client identifiers.
The following recommendations mitigate threats owing to compromised access tokens:
- “Per user per client” throttling limits.
- One-time-use access tokens / rolling access tokens / access token chaining.
- Heuristic algorithms to detect token fraud.
The following recommendations mitigate threats due to compromised refresh tokens:
- One-time-use refresh tokens / rolling refresh tokens
One-time-use Client Identifiers / Rolling Client Identifiers
With one-time-use client identifiers / rolling client identifiers, each time you load the SPA from the backend, after the OAuth 2.0 authorization redirect, you get a new client identifier embedded in the JavaScript. Each new client identifier is valid only for one use and is invalidated at its first use at the authorization server. Each instance of the SPA will have its own client identifier, instead of sharing the same client identifier in all the instances. The sequence of client identifiers generated is structured in a parent-child hierarchy at the authorization server, which allows it to easily trace the sequence of client identifiers generated for that particular SPA. The throttling limits are enforced on this first client identifier.
This makes it harder for a rogue client application to steal a client identifier to get an access token using it and eat-out the throttling quota of the legitimate application. In order to do so, for each request, it has to load the legitimate SPA and scrape through it to find the next valid client identifier. This kind of attack can be mitigated by enforcing denial-of-service protection measures at the SPA, and possibly deny listing the rogue client.
Per-user per-client Throttling Limits
By enforcing “per-user per-client” throttling limits, instead of only “per-client” throttling limits, a rogue client can only eat-out the throttling quota of the user whom the access token was granted to. All other legitimate users of the same application are not affected.
One-time-use Access Tokens / Rolling Access Tokens
With one-time-use access tokens/rolling access tokens/chaining access tokens, an access token can only be used once at the resource server to access protected resources. On its first use, it is invalidated and a new access token is returned. The sequence of access tokens generated is mapped to the first access token in the authorization server. In order to get a new access token, the immediate previous invalidated access token has to be provided.
If the authorization server finds that the sequence of provided tokens is broken at any point in time, it will invalidate all the access tokens in the sequence. To make this even more effective, the lifetime of the access tokens have to be as short as possible.
Heuristic Algorithms to Detect Token Fraud
Existing token fraud detection methods rely largely on heuristic algorithms such as tracking sudden changes in IP addresses, browser or mobile fingerprinting, and flagging “unusual user behavior”. Unfortunately, these methods themselves can be inaccurate, easy to spoof, and difficult to implement.
One-time-use Refresh Tokens / Rolling Refresh Tokens
This recommendation comes straight from the “The OAuth 2.0 Authorization Framework” (RFC 6749). This was in fact the one and only recommendation available to mitigate the risk of refresh token breach at the time OAuth 2.0 was introduced. This recommendation would become indispensable in OAuth 2.0 public clients.
The idea is to invalidate the current refresh token and issue a new one whenever the refresh token grant flow is used. The previous refresh token is invalidated but retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach.
Though this looks good on paper, it can become quite challenging for CSAs to implement this in practice. Imagine if you have multiple client instances sharing the same token store and you need to implement serialized access to the shared resource. Or else imagine what happens if you successfully use a refresh token but fail to receive a new one.
Non-standard Solution Patterns for SPAs
The following non-standard solution patterns are often prescribed to overcome limitations in the existing standards:
- OAuth 2.0 Proxy Patterns
- Stateful OAuth 2.0 + API Proxy
- Stateless OAuth 2.0 + API Proxy
- Sender-constrained tokens
- Split Access Token Cookie Pattern
- Binding Token Cookie Pattern
Stateful OAuth 2.0 + API Proxy
In this solution pattern, we try to introduce a server-side component that acts as an OAuth 2.0 proxy, which can perform the authorization code flow in addition to keeping the client credentials, access tokens, and refresh tokens confidential within the control perimeter of the application.
Figure 1: Stateful OAuth 2.0 + API Proxy for SPA
The idea is that the OAuth 2.0 proxy component will function like a usual server-side application that performs the authorization code grant flow in order to obtain the ID, access, and refresh tokens.
The user information obtained from the ID token and the OIDC userinfo response will be returned to the browser, either as part of the authentication response DOM when loading the SPA from the HTTP Server to be read on boot, or as a secure non-http-only cookie that can be read by the SPA.
The access token and refresh token are stored in the server side in a mapping table against a UUID value. The UUID value is sent back to the browser as the value of a secure http-only session cookie.
Whenever an API invocation needs to be made from the frontend client, it will invoke a corresponding backend proxy URI along with which the session cookie is sent. The backend proxy will, in turn, find the corresponding OAuth 2.0 access token for the UUID, do the actual invocation to the API server, receive the response, and pass it on to the frontend client.
If the access token is expired, the proxy component will receive an access token expiry message from the API server. Thus it will immediately perform the refresh grant flow to obtain a new token, do the invocation to the API server again, receive the correct response and pass it on to the frontend client. The frontend client is oblivious to the access token refreshing process.
Another benefit the OAuth 2.0 backend proxy patterns provide in general is eliminating the need for the business APIs to support cross-origin resource sharing (CORS), as the API invocation to the business API provider is initiated from the OAuth 2.0 backend proxy.
The application is no more technically an OAuth 2.0 public client, which means:
- The application can now be authenticated legitimately using its client credentials.
- The access token can be now stored securely in the applications backend proxy component.
The main drawbacks of this solution pattern are:
- This OAuth 2.0 proxy must maintain state, such as the mapping between the frontend client and the client credentials and token information, which comes with scalability issues and will introduce additional complexity to the solution. Typically you would have to use a database to share this information with multiple OAuth 2.0 proxy components.
- Now the application is not a pure client-side application anymore as we’ve introduced a backend component to it.
Stateless OAuth 2.0 + API Proxy
In this stateless variation of the OAuth 2.0 proxy pattern, we follow the same procedure as the previous pattern, however, the token information is stored as an encrypted value in a session cookie in the frontend client. The plain text value of the cookie could be an unsigned JSON web token (JWT).
Figure 2: Stateless OAuth 2.0 + API Proxy for SPA
All the API invocations will go through the backend proxy as with the stateful variation and carry the encrypted cookie, which will be decrypted in the backend proxy and then used similar to the stateful OAuth 2.0 proxy pattern.
The point to keep in mind is that, though from the authorization server’s point-of-view the tokens will be marked as issued to a confidential client, the actual security characteristics of the client are that of a public client. Whatever API you are calling using this mechanism should NOT use the client type as a decision factor for authorization. Alternatively, the AS should provide confidential clients with a mechanism to signal that the requested token will not be used by them, so that the token bits can reflect it accordingly.
The benefit of using this pattern over its stateful variation is that the tokens are stored in the client-side and therefore there is no requirement to maintain state in the backend, and won’t run into scalability issues. Since the token is encrypted, an attacker cannot steal the plaintext access token or refresh token, and call the API server directly bypassing the OAuth 2.0 proxy.
However, you may notice that there is nothing preventing a rogue client or attacker stealing the encrypted cookie and making an API invocation.
The “Token Binding over HTTP” (RFC 8473) [2] specification that emerged relatively recently could potentially be used between the browser and the OAuth 2.0 backend proxy, to bind a cookie to a particular client instance that was used to make the request as a result of which the cookie was stored in the browser. This means that even if an attacker obtains such a cookie, he or she would not be able to use it.
Unfortunately, this specification suffered an important setback when Google announced that they would be dropping the support for it in the Chrome browser [3], the leading browser in terms of market share, and iOS never committed to implementation.
Summary
In this post, I discussed the challenges in CSAs, standard and non-standard recommendations that are widely employed to overcome those challenges, and some of the non-standard solution patterns for SPAs to overcome limitations in the existing standards I discussed in part one, specifically regarding OAuth 2.0 proxy patterns.
Always beware when implementing any solution that isn’t prescribed in any public specification or public threat model. You would have to be responsible for a critical component of your architecture, without the benefit of following a design that has been validated by a large community, or without the chance to use off-the-shelf components implementing the feature.
In the next part, I will discuss the important security properties of cookies that make them a suitable candidate for a sender-constrained token technology and more non-standard solution patterns, specifically sender-constrained token patterns.
Read the third and fourth articles of this series to learn more.