I manage an open source implementation of OAuth 2.0 (along with OpenID Connect and a bunch of extensions) called MITREid Connect. As the name suggests, it started out as a project during my tenure at MITRE, and we were looking to build an enterprise-class open source implementation of these new protocols for use at our company and beyond. It’s been a fairly successful project; people are using it all over the place and, most interestingly, customizing it to fit into a wide variety of environments. With that use comes bug reports and feature requests (and the occasional pull request), and we have seen the core project grow and develop in new and interesting ways. But one request has come through enough times that I wanted to take some time to talk about it:
How do we get a refresh token from the server using the implicit flow?
It seems like a reasonable request on the surface, but this question almost always comes about because the person asking it is doing something fundamentally wrong with their OAuth setup. How is that? First, we need to understand a few things about the implicit flow and refresh tokens.
What’s the Implicit Flow?
When OAuth 1.0 was written, one of the goals was to have a single way of doing things for every type of application. That way, it’s argued, everything is guaranteed to be compatible and interoperable with everything else. It turned out that even with that goal, people weren’t doing everything quite exactly the same way. When it came time to write OAuth 2.0, we embraced this flexibility and defined a set of different “grant types” or “flows” for different use cases. This gives us not only a core way to do things — the authorization code grant type — but also several optimizations designed for specific circumstances and use cases.
The “implicit flow” (more properly the “implicit grant type”) is an optimization designed for use by OAuth clients executing inside of a web browser. The authorization code grant type goes to great lengths to separate the information known by and exposed to the client application and the browser, and is very careful that these paths don’t cross. However, when the client is running inside the browser, these distinctions don’t make much sense anymore. The implicit flow can simplify things and allow the client to get its access token in one step — thereby doing away with authorization codes and client secrets and other messy security things.
That simplicity comes at a price, of course: the implicit flow is inherently less secure than other forms of OAuth 2.0. For instance, it doesn’t allow for the client to authenticate itself to the authorization server. It also leaks information to the browser, which is a general-purpose application with lots of potential threats. However, the implicit flow does inherit a lot of the security of the browser itself, relying on protecting redirect URIs, same-origin policies, protection of the URI fragment from remote systems, and other browser-specific things to make it work and a reasonably secure way given its environment. And, since the client only lives as long as the browser’s session, you can start to make some limiting assumptions about the running environment of the client to help your security posture overall, such as tying access tokens to the session lifetime of the user.
What’s a Refresh Token?
In OAuth 1.0, developers generally assumed that access tokens lived forever. They didn’t, of course — sometimes they expired, and often they got revoked. In OAuth 2.0, we ensconced the expiration mechanism to allow applications to limit the exposure caused by a leaked access token. But what’s a client to do when the access token expires before the client is done calling its API? You could always just do the OAuth dance again, and that’s fine, but an OAuth “refresh token” is a mechanism that lets a client fetch a new access token without bothering the user (or requiring the user to be present).
Refresh tokens are very handy things, since the user’s grant of authorization is likely intended to last longer than the access token itself. Normally, a client would need to pop open a new browser window and get the user to log in and authorize the application again, a process whose friction is likely to scare away repeated users of an application. Instead, we want to enable the “Trust” in the “Trust On First Use” principle in our applications, so we’ll get a refresh token alongside our access token. That way when the access token expires, we can use the refresh token to get a new access token without bothering the user.
What’s All This Got to Do With Mobile Applications?
Here’s where things start to get quirky. You see, a lot of developers look at the implicit flow’s simplicity and think, “You know, that’s a lot easier to build so I’m just going to do that in my application instead of all this authorization code silliness.” And they’re right about that point, it’s simpler . But remember what we discussed above: that simplicity comes at a cost of assumptions about the environment in which the client is running. But we’re still using OAuth, so it must be secure, right? No, not at all. Use the implicit flow outside of that specific environment, and all of the assumptions upon which its security is built suddenly fly out the window.
This confusion is further compounded by native applications, such as those on mobile devices. Since native apps get installed on a large number of different devices, traditional methods for managing client secrets don’t apply anymore. Since the implicit flow doesn’t use a client secret, it seems like a natural fit for native applications.
And that’s where the problems start. With an in-browser application, the application ceases to exist when the user goes away and closes the browser. However, that’s not true with a native application: the browser is going to get closed and the app is going to continue to function long after the OAuth process has completed. If we’re already inside of a browser session, it’s no big deal to redirect the user to the AS to get a new access token. But in a native app, it’s going to be an awful and error-prone experience. Therefore, it makes sense to use a refresh token with native apps and avoid this pain.
So why can’t we just tack a refresh token onto the implicit flow and admit that it’s getting used beyond where it was first designed? If this were our own problem, that might be a good solution. As it turns out, there are other problems with using the implicit flow on a native app, so patching the user experience with a refresh token would just be making bad security easier to use. We can do better than that.
Stealing the Redirect
In the OAuth protocol’s authorization code and implicit flows, the authorization server communicates the results of the authorization decision back to the client by attaching query parameters to a redirect URI and making the user’s browser fetch that URI. In a web-based application, either hosted on a remote server or running inside of a browser, the redirect URI is going to be coming from a server controlled by and directly associated with the application. In fact, the security of OAuth in these kinds of applications depends largely on this.
Native applications are different, though. Since interaction with a native application doesn’t happen through the user’s browser, we need another way to get information back to the client using the browser. There are a number of ways to do this on modern platforms, but the most common ones amount to telling the browser (and the associated operating system) that when it sees a URI of a particular pattern, it should open a call to a specific application instead of making an HTTP request.
The problem comes when you realize that unlike with a remote server URL, there is no reliable way to ensure that the binding between a given redirect URI and a specific mobile application is honored. Any app on the device can try to insert itself into the redirection process and cause it to serve the redirect URI. And guess what: if you’ve used the implicit flow in your native application, then you just handed the attacker your access token. There’s no recovery from that point — they’ve got the token and they can use it. They hardly had to do any work to get it, either — you made it very easy for them. And if you revoke that token, it’s going to be just as easy for them to get a new one.
Now, if you’re using the authorization code flow, then we’ve got a chance to save you. Unlike the access token, which can be used directly without access to any other credentials, you can only use the authorization code if you have access to the client’s credentials. This is great, and in fact it’s what saves web applications from impersonating each other: the client secret is stored on the web server, and out of the hands of the browser.
But there’s a problem here, too: native applications aren’t good at keeping client secrets. In fact, if you’ve got a native application installed on a thousand devices, and all of those devices use the same secret, then your secret is hardly a secret anymore. You can use OAuth Dynamic Client Registration to give each copy its own secret, and that’ll solve the security problem. Dynamic registration works well, but not everyone wants that solution for a variety of reasons including policy and management. Thankfully there’s a clever alternative: we’ll use pixies.
Specifically, we’ll use Proof Key for Code Exchange, shortened to PKCE and pronounced “pixie”. In PKCE, the client creates an unguessable value and hashes it. The authorization request includes the hashed value, but the request for the token must include the original unguessable value. As such, any attacker that can sit on the redirect URI would be able to steal authorization codes, but they wouldn’t be able to use them. And since we’re using the authorization code flow, the attacker never sees our legitimate call to get the access token.
The Right Tool
Hopefully now you see why every time we’ve gotten the request to add refresh tokens to the implicit flow’s response in our OAuth server, we’ve told people “no.”
In the end, OAuth 2.0 has a lot of flexibility, but that flexibility comes at the cost of needing to decide on the right tool for the right job. It’s not enough to just grab part of the OAuth 2.0 protocol family and apply it, thinking that since you’re doing OAuth you’re being secure. If you’re writing a native application, use the authorization code flow with PKCE. And, in fact, the OAuth Working Group is working on a guide document for these and other best practices for native applications. You’d do well to not only follow that advice, but also to join the conversation and add your experience to the stack for others to learn from.