Reusing Refresh Tokens By Default
Historically, IdentityServer could either issue reusable refresh tokens or enforce refresh token rotation. The default value was “rotate” which can often lead to problems. In IdentityServer 7.0, we made the decision to change the default behavior of refresh tokens so that they would be reusable by default. In this blog post, we’ll describe refresh tokens and their security in detail and explain why we made this choice.
What is a refresh token?
Refresh tokens are a mechanism that allows client applications to obtain new access tokens for an interactive user without redirecting the user to the identity provider. This allows clients to obtain long-lived access that can be revoked, without needing to redirect back to the identity provider. Preventing unwanted redirects back and forth with the identity provider improves the user experience, both because redirects would cause the UI to flicker, and more importantly because the user may have state within the application that could be lost during a redirect. Imagine if you filled out a long form, only for the application to redirect away to the identity provider when you submitted that form, and you had to reenter all that information.
The solution to this problem is not to make the access token long lived however. Access tokens need to be short-lived because they are typically self-contained JSON Web Tokens (JWTs), and will be valid as long as we specify when we create them. The self-contained nature of JWTs is a big advantage in that it makes tokens easy to validate in APIs. But, this also means that the identity provider has nothing more to say once a token has been issued. If the token had a very long lifetime, that would mean that whatever access we grant in our access token would be unchangeable for that long duration. Typically we want to be able to enforce authorization rules relatively quickly. For example, we might wish to revoke access when a user is deactivated, or enforce that access has changed due to a change in policy.
The better way to handle this situation is to issue a short lived access token and a long lived refresh token. This gives the benefit of a self-contained (easily validated) access token, but forces the client to periodically return to the identity provider to ask for a new token. Each time it does so gives the identity provider the opportunity to enforce authorization policy.
IdentityServer’s refresh token usage types
IdentityServer’s refresh tokens can either be reusable or rotated. Reusable refresh tokens do exactly what they say on the tin: the client application uses and reuses the refresh token each time it needs a new access token. In contrast, rotated refresh tokens are only usable one time. When a rotated refresh token is used, a new refresh token is issued in addition to the new access token. The client application must use the most recent refresh token in the chain. All previous tokens have already been used and cannot be used again. The intention of this technique is to prevent an attacker from replaying a refresh token and to give the identity provider a mechanism to detect that multiple refresh requests have occurred, which might suggest that the client has been compromised. However, as we’ll see below, sophisticated attackers can bypass this technique pretty easily.
Security Guidance from the Specifications
Refresh tokens can be a high-value target for an attacker, because they potentially allow for long-lived access to resources. This means we should take special care to ensure that refresh tokens are handled safely, and indeed many of the specifications from the IETF contain guidance on how to handle refresh tokens, including
- OAuth 2.0 Security Best Current Practice
- OAuth 2.0 for Browser Based Apps
- OAuth 2.0 for Native Apps
- OAuth 2.1
If you read those specs carefully, you’ll notice that the security considerations are significantly different for confidential and public clients. Confidential clients already must authenticate themselves during the refresh process. An attacker would not be able to use a refresh token that was issued to a confidential client without the client secret. However, public clients cannot authenticate themselves, meaning that an attacker would be able to use a stolen refresh token that was issued to a public client unless other security measures are taken. To defend against this, the specifications have historically recommended either rotation or sender-constraining refresh tokens issued to public clients.
Why Rotation Doesn’t Help
Of those two options, rotating tokens is not a very effective strategy, and new security research shows why. A sophisticated attacker can simply wait for the user to stop using their session or sign out before commencing their attack. The attacker will watch the rotations occur until they stop, and then use the last token in the chain of rotated tokens in the attack. Philippe de Ryck gave a great talk on this subject where he demonstrates the technique. He’s also one of the authors of the OAuth for Browser Based Apps best current practice document from the IETF, which unsurprisingly gives similar advice about rotation being easily bypassed.
On top of that, rotation comes at the cost of database pressure and reliability problems. It creates database pressure because every rotation necessarily involves deleting or marking as consumed the used token and writing the new token. It creates reliability problems because when the http response with the rotated token fails (when not if, because as we all know, the network is unreliable), the client application has no way to recover other than to force the user to login again.
Sender Constraining Tokens
Sender-constraining tokens has historically been pretty challenging, as the first standardized mechanism to do so relied on mutual TLS to authenticate the client. The infrastructure needed for mTLS is a pretty big undertaking, and managing client certificates is probably not feasible if your app is used by the general public. However, IdentityServer has supported the newer DPoP protocol for sender-constrained tokens since version 6.3. DPoP doesn’t require the same public key infrastructure as mTLS. Instead, in DPoP, the client proves it has possession of a secret by creating a “proof token” - essentially a JWT that is signed with a private key known only to the client application. The identity provider then issues tokens that can only be used with a new proof token signed by the same secret. DPoP works great in native apps because the major platforms all provide OS level secure storage of secrets. Your native app can generate and store a DPoP proof key, protected by the OS’s secure storage. For example, on an iPhone the user would have to unlock with biometrics in order for the application to access the DPoP proof key.
SPAs and the BFF pattern
The other main type of public client is a SPA. Public clients running in the browser can use DPoP to partially defend against some attacks. Notably, attacks where the application’s tokens are exfiltrated can be prevented using DPoP. However, an attacker able to inject malicious code into a SPA can run a new authentication flow using a DPoP key controlled by the attacker. This results in tokens that the attacker can use that have effectively bypassed the protection of DPoP. Instead of issuing tokens directly to a SPA and then attempting to sender-constrain those tokens, we recommend using the BFF architecture. Essentially, the SPA should not be an OAuth client at all. Instead, in line with guidance from the OAuth for Browser Based Applications spec, we recommend that browser based apps be paired with a back-end that is a confidential client. We have a library for implementing this pattern that we highly recommend.
Conclusion
So to sum up: Rotation doesn’t actually provide security benefits, it comes at the cost of reliability and database pressure, and it isn’t needed for confidential clients at all. For all those reasons, we’ve changed our default behavior to make refresh tokens reusable. Our advice in general is to:
- Use confidential clients everywhere that you can,
- Use the BFF pattern with SPAs to avoid browser based applications that are public clients, and
- Use DPoP in native applications to sender constrain your tokens.