This blog post is a summary of my interpretation and perspective of what’s been going on recently with the implicit flow in OAuth2, mainly spurred on by the recent draft of the OAuth 2.0 for Browser-Based Apps (which I will refer to here as OBBA) and the updated OAuth 2.0 Security Best Current Practice (which I will refer to as the BCP) documents from the OAuth2 IETF working group. These are still in draft, so it’s possible they might be changed in the future.
This is a long post because these new documents have forced the community to rethink the security practices we’ve been using for several years now.
A brief history of the implicit flow
This trepidation was documented in the RFC6819, the OAuth 2.0 Threat Model and Security Considerations spec. In fact, many threats for all the flows are covered in that RFC, and any decent client and token server implementations should heed the advice (for example, using the state parameter for cross-site request forgery (CSRF) protection, exact redirect URI matching, etc.). But the aspect of the implicit flow that is most criticized as difficult to protect is also the fundamental mechanic of what defines the implicit flow, namely that the access token is returned from the token server to the client from the authorize endpoint.
One question that would commonly be asked about making browser-based clients more secure is “what about code flow – why can’t we use that instead?”. It turns out code flow (by itself) was worse because 1) public clients don’t use a real secret to exchange the code at the token endpoint, so an attacker could just as easily steal the code to obtain the access token, 2) codes passed via the query string are sent to the server (whereas fragment values are not), so they would be exposed more than when using implicit flow, 3) the client is required to make more requests to complete the protocol for no additional security, and 4) to use the token endpoint the token server would need to support CORS, and CORS was not yet widely enough supported by browsers. At that time, the spec designers could not take a dependency on CORS thus they had to find an alternative. I think this is (at least) one of the main reasons implicit as a flow was originally devised.
The spec committee has long wanted something built into the protocol itself to help protect against this threat of access token leakage from the URL. There are numerous posts on the working group email list that discuss this (from the time OAuth2 started being developed in 2010 and since it’s completion in 2012). But, at the time, the implicit flow was the best they had to offer. It was important that they provided some guidance rather than no guidance for fear of people inventing their own security protocols.
And just so we’re all clear on the value of specifications; they are pre-vetted threat models. That’s why we like them and (typically) follow them, because there is a high level of scrutiny, many people have thought about the attacks, documented the approaches we can take to mitigate them, and educated us on the current known issues for the types of activities we’re trying to perform. Without them each developer would have to come up with their own security and threat model it and that historically hasn’t sufficed.
A new hope
Nonetheless, in the working group there was still a desire to “do better” when it came to the implicit flow. Looming on the horizon was hope: HTTP token binding (first introduced in 2015). HTTP token binding was a new spec that would (basically) tie the token to a particular TLS connection ensuring only the rightful client could use that token. This provided a solution to address the concerns about exfiltrating tokens from the browser (and other types of clients too). Things were looking up and the future of security was good. In fact, work was in progress in 2016 to incorporate this new token binding feature into the OAuth2 protocol, but then, unfortunately, in 2018 Google made the decision to drop support for this “unused feature” in their Chrome browser. This effectively made token binding impractical for browser-based clients (despite the final token binding RFCs being completed in the same month).
Around the same time (in 2015) the OAuth2 working group devised RFC7636 Proof Key for Code Exchange by OAuth Public Clients (also known as PKCE) to address an attack against native clients. The attack involved stealing the authorization code as it was being sent back to the client in the redirect URI, and since public clients don’t have a real secret then the authorization code issued was as good as the access token. The mitigation used in PKCE was to create a new dynamic secret each time a client needed to connect to the authorize endpoint. This dynamic secret would then be used on the token endpoint and the token server would help guarantee that only the rightful client could use the code to obtain the corresponding access token.
I don’t think anyone in the OAuth2 working group anticipated it, but PKCE turned out to be useful for all types of clients not just native ones. Given that token binding had fizzled, the idea of using code flow with PKCE became a candidate to address the issue of access tokens being exposed in the redirect URI for implicit clients. Also, by this time CORS had finally become well enough supported that it could be utilized for these browser-based clients. This confluence is what seems to have galvanized the work on the documents that this blog post is about.
The OAuth 2.0 for Browser-Based Apps document
And having thought about it, despite having all the existing mitigations for implicit flow, I agree that code flow with PKCE is valid advice and an improvement. Justin Richer, on the working group email list, summarized it the best in my opinion:
“The limitations and assumptions that surrounded the design of the implicit flow back when we started no longer apply today. It was an optimization for a different time. Technology and platforms have moved forward, and our advice should move them forward as well.”
As such, I have updated my OIDC certified client library oidc-client-js to support code flow with PKCE as of 1.6.0 (released in December 2018). I will follow up with another blog post on those details.
Other recommendations in OBBA
The somewhat surprising recommendation in OBBA for same-domain applications is to not use OAuth2 at all and use the old approach of using cookies to authenticate to the backend API. Suggesting an approach that makes security worse seems counterproductive, but the recommendation is based on yet another recent security specification: Same-site Cookies. Same-site cookies allows a web server, when issuing a cookie, to instruct the browser to only send the cookie when requests comes from the domain that issued the cookie. This behavior mitigates the CSRF attack.
Given that same-site cookies have sufficient browser support now, it seems practical to rely upon it for our CSRF protection and thus this recommendation in OBBA is also convincing. But at the same time it’s sort of ironic that the OAuth 2.0 for Browser-Based Apps best practices document suggests not using OAuth.
Anyway, this doesn’t help you if your API must be accessed by clients on different domains or from clients that aren’t running in the browser, so using OAuth2 and token-based security for your API for those scenarios is still appropriate.
Token renewal and refresh tokens
Access tokens expire and client applications need some user-friendly mechanism for renewing those access tokens. Prior to OBBA, a common renewal technique for implicit clients would was to make a new request to the authorization endpoint but using a hidden iframe. In fact, the OIDC spec even added a provision for this style with the prompt=none authorization request parameter. This relied upon the user’s authentication session (typically in the form of a cookie) at the token server for this to succeed. This approach works fairly well (if you can get over your incense of still using an iframe in this day and age). Recall, though, that when the implicit flow was developed CORS was still not a viable option. And even with the updated guidance in OBBA, the iframe approach is still a valid approach.
The practical effects of OBBA and BCP
The OBBA has language that seems to contradict the earlier statement about not using refresh tokens. The OBBA and the BCP (the other document this post is about) both indicate that you need to evaluate for yourself if you want to use refresh tokens in browser-based clients.
To their credit, both documents provide guidance on ways to protect the refresh token in the browser and mitigate abuse by an attacker. The main mitigations include using a client-bound refresh token and/or performing refresh token expiration and rotation when the refresh token is used. Unfortunately, these mitigations might not be available based on the situation. Having said that, I also added refresh token support to oidc-client-js in 1.6.0, including renewal and revocation. Again, more about that in another blog post.
If you wish to use refresh tokens in the browser, this means you must use a token server that has the mitigation features described in these documents. Not all token servers support these recommendations. I believe one intended audience of these documents is the token server vendors. But on the working group email list I see some vendors that express concern that they will not be able to accommodate the approaches recommended (or at least not soon). Having said that, IdentityServer already has support for many of the recommendations, and we are making plans to add additional mitigations.
This escape clause is the concern I have with these documents. I’m left uneasy with the burden now being back on the developer to decide to use refresh tokens and evaluate their token server’s support for mitigations for use with browser-based clients. The developer will need to know how to use them, configure them properly, know how to protect them in their client, and how to threat model those decisions. We know that most developers are not security specialists. From that perspective, perhaps this is a step backwards in terms of improving overall security. I suspect we will see compromises in the future based on refresh tokens being used in the browser.
Where are we?
Same-domain apps with cookies
This style would be for same-domain applications that use same-site (and HTTP-only, as always) cookies to authenticate to an API backend. The backend would issue the cookie based on the user’s authentication (which itself could be as a result of SSO to an OIDC token server), and cookies would be renewed while the user is still active in the client. This style of application would use well-established approaches for securing the client, including CSP. This style is admittedly the easiest for the developer. The main downside might be the requirement to run in older browsers.
OAuth2 clients without using refresh tokens
This style would be for browser-based clients that need to use cross-domain APIs, or an API that only accepts tokens (and doesn’t support cookies). This client would use code flow with PKCE to obtain the access token, but the rest would be essentially the same as an implicit client would do today, including using an iframe to renew access tokens. Again, standard approaches should be used to secure the client application (including CSP). The token server will need to support CORS and PKCE, and the ability the renew tokens is based on the user’s session at the token server.
OAuth2 clients using refresh tokens
This style is essentially the same as the previous, except that refresh tokens would be obtained by the client and used to renew access tokens. To mitigate the attacks against the refresh token being leaked the token server needs to support some sort of client-bound refresh tokens, or a refresh token expiration and rotation strategy.