The Problem: Security Tokens in the Browser

  1. The Problem: Security Tokens in the Browser
  2. The Solution: Easier and More Secure With Authentication Gateways

TLDR; Authentication gateways shift the use of security standards such as OAuth2 and OpenId Connect to the server side. This not only drastically simplifies the implementation of the SPA, but also makes the solution considerably more secure.

It was the end of 2012, and nobody was really interested in cookie-based logins anymore. This approach from the 90's didn't seem to fit well to single page applications, nor to stateless REST services.

In this respect, it is not surprising that more and more single-page applications started handling security tokens directly. OAuth 2 that had just been released at the time was the standard of choice. Together with OpenId Connect (OIDC), which was released around 1.5 years later, a standard-compliant and effective method for the authentication and authorization of users was available. Both standards are nowadays supported by almost every security provider.

While this may sound like the perfect happy ending to a fairy tale for software developers, the story isn't over yet. The OAuth 2.0 working group collected experiences we gained over time and wrote some best practice documents which should also form the basis for the upcoming OAuth 2.1. While the document OAuth 2.0 Security Current Best Practices describes general best practices, OAuth 2.0 for Browser based Apps focuses on browser-based applications and thus on SPAs.

These documents leave no stone unturned in some places and even cookies are recommended again for some scenarios. In this article, I address these new perspectives. I show how token-based security can be combined with cookies and what advantages and consequences result from this.

Tokens in the Browser: The Implicit Flow

One goal of OAuth 2 and OIDC is to serve as many types of clients as possible. In addition to SPAs, this also includes classic web applications, native applications, but also solutions on restricted IoT devices. Since all of these clients have different characteristics and options, the standards provide for different flows. Flows describe messages that are exchanged between client and authorization server and ultimately lead to the issuance of the token.

Due to the limited options in the browser, OAuth 2 and OIDC for SPAs provide a greatly simplified flow:

OAuth 2 and OpenId Connect in the browser

This so-called implicit flow defines that the SPA redirects the user to an authorization server for authentication. The specific type of authentication is an implementation detail that the standards do not specify. In simple cases, username and password are used. Among other things, different types of 2FA, Windows authentication for automatic recognition of the current Windows user or forwarding to other authorization servers can also be used.

After that, the authorization server redirects the user back to the SPA. All this is also known from other flows. However, a special feature of the implicit flow, which has been simplified for browser applications, is that the authorization server includes the issued tokens directly in the URL of the SPA. The SPA takes it from there and thus gains access to the backend (resource server).

As a supplement, OIDC provides an Identity Token (Id-Token) that informs the client about the logged-in user. It contains key/value pairs -- so-called claims -- which describe the user. A user info endpoint provides further information on the current user. This is a REST service described by OIDC.

The Crux With Tokens in the Browser: Code Flow as a Solution?

The use of the implicit flow is not entirely unproblematic. Above all, the fact that it sends tokens in the form of URL parameters is (not only) viewed extremely critically by the current best practice documents. URL parameters are stored in the browser history and in logs on the authorization server and, if used, on proxy servers. In this respect, attackers have a few ways to get to the tokens.

We were aware of this fact from the start. Therefore, it was common to overwrite the browser history with JavaScript and configure logging accordingly. The situation is similar with other problematic features of the implicit flow which have been known for a long time including suitable countermeasures.

This does not mean that the implicit flow is insecure per se. However, numerous defense mechanisms are necessary to protect against attackers. Since the chain always breaks at the weakest link and there are good alternatives, the best practice documents mentioned prohibit the use of the implicit flow.

Instead, the so-called Authorization Code Flow , which probably best reflects the ideas of OAuth 2, is to be used:

Authorization code flow in the browser

Here, the authorization server only sends the SPA a one-time code instead of the tokens. The SPA exchanges this for the actual tokens via an encrypted HTTPS connection. To prevent attackers from intercepting this one-time code and using it themselves, the best practice documents also prescribe the use of PKCE (Proof Key for Code Exchange). This standard ensures that only the client who originally initiated the flow can redeem the code.

Even More Crux: Token Refresh in The Browser

In addition to the challenges that can be solved by switching to the Authorization Code Flow and PKCE, there is another fundamental problem: browsers are not able to store tokens securely. There are numerous approaches to cross-site scripting and thus to hacking into an SPA. The fact that this threat should be taken seriously is also shown by the OWASP Top 10 from 2021, where injection attacks such as cross-site scripting are in third place.

For this reason, it has long been good practice to work with short-lived access tokens. A lifespan of 10 +/- minutes is quite common. If an attacker manages to steal the token, his radius of action is limited.

However, the use of short-lived access tokens also means that our SPA has to obtain a new access token from the authorization server on a regular basis -- and if possible without user interaction. After all, users don't want to have to enter their password every 10 minutes again. Unfortunately, there is no really good solution for this.

An approach that is often found is a so-called silent refresh in a hidden iframe:

Silent refresh with HTTP-only cookie and hidden iframe

By using a cookie, the authorization server remembers the logged-in user. This means we can do without additional user interactions. Since this cookie is HTTP-only, an attacker cannot steal it via a JavaScript-based attack.

Another implicit or code flow takes place in the hidden iframe. The user is therefore not aware of the associated redirection to the authorization server. Although this approach conforms to the standard, it is a workaround at the end of the day and error-prone due to the use of iframes. In addition, this use of iframes prevents the use of SameSite cookies, and SameSite cookies in particular are an important means of limiting XSRF attacks.

Actually Forbidden: Refresh Tokens in The Browser

An alternative approach for token refresh in general is the use of refresh tokens:

Originally banned for a good reason: refresh tokens in the browser

Clients can use refresh tokens to request another access token, identity token, and refresh token. According to the original specifications, however, this is exactly what is forbidden in the browser: If an attacker gets their hands on the refresh token, they can use it to retrieve new access tokens in the long term and thus act longer on behalf of the user.

The current best practice documents are not quite so restrictive. However, they emphasize that a risk assessment should be carried out before using refresh tokens. This will take into account the likelihood of refresh tokens being stolen and hence the likelihood of cross-site scripting and the damage that will result.

The best practice document for OAuth 2 in browser-based applications also requires that refresh tokens must either be user-bound or replaced with a new refresh token when redeemed. The former is currently only possible when using client certificates. In the latter case, there is also talk of a "rotation" of the refresh token.

Despite rotation, there must be a maximum life span in which refreshes are possible and a refresh token can also expire if it has not been used for a certain period of time. If two clients use the same refresh token, there is a risk that one of them will be an attacker. In this case, the authorization server may neither accept the refresh token nor other refresh tokens requested with it.

More on Angular Security

More on this and other advanced security topics in the world of Angular can be found in our Angular Security Workshop with the international security expert Dr. Philippe De Ryck. It's 100% online and interactive with a mix of lectures, demos, quizzes, and hands-on labs.

Angular Security Workshop with Dr. Philippe De Ryck

All Details: Angular Security Workshop

Conclusion

As shown in this article, the browser is no save place for storing tokens. Also, there are no really good ways for performing a token refresh in Single Page Applications.

For these reasons, the OAuth 2.0 working group recommends server-side OAuth 2.0 and server-side token handling. More information on this solution can be found in the 2nd part of this article series.