Refresh tokens provide a UX friendly way to give a client long-lived access to resources without having to involve the user after the initial authentication & token request. This makes them also a high-value target for attackers, because they typically have a much higher lifetime than access tokens.
This post is not about whether you should store access or refresh tokens in a browser (you probably know my opinion about that already), but rather on how to reduce the attack surface of refresh tokens – SPA or not.
It’s a good idea to ask for consent when a client requests a refresh token. This way you at least try to make the user aware of what’s happening, and maybe you also give them a chance to opt-out of it. While this will go at the expense of the user experience, but maybe this is OK for the user.
In IdentityServer we always ask for consent (if enabled) if the client asks for the offline_access scope which goes in-line with the OpenID Connect spec.
Refresh tokens usually have a (much) longer lifetime than an access token. You can reduce the exposure though by also adding a sliding lifetime on top of the absolute lifetime. This allows for scenarios where a refresh token can be silently used if the user is regularly using the client, but needs a fresh authorize request, if the client has not been used for a certain time. In other words, they auto-expire much quicker without potentially interfering with the typical usage pattern.
In IdentityServer we support sliding expiration via the AbsoluteRefreshTokenLifetime and SlidingRefreshTokenLifetime client settings.
One-time Refresh Tokens
Another thing you can do, is rotating your refresh tokens on every usage. This also reduces the exposure, and has a higher chance to make older refresh tokens (e.g. exfiltrated from some storage mechanism or a network trace/log file) unusable.
The downside of this approach is, that you might have more scenarios where a legitimate refresh token becomes unusable – e.g. due to network problems while refreshing them.
In IdentityServer this is supported via the RefreshTokenUsage client settings, and we default to one-time only usage.
On top of one-time only semantics, you could also layer replay detection. This means, that if you ever see the same refresh token used more than once, you could revoke all access to the client/user combination. Again – same caveat applies – while increasing the security, this might result in false positives.
We will add this feature in version 4 of IdentityServer.
Additional Token binding
Another technique to consider could be the binding of the refresh token to the client using key material. In access tokens we have the cnf claim for this purpose.
You could apply the same technique to refresh tokens, in other words, if the client has presented some proof key at token request time, refreshing the token could require the same key again. This would go well with the ephemeral MTLS client certificates I wrote about in this blog post. I could see this as a nice additional tool on your belt, especially for public mobile or desktop clients.
There is currently an open issue to define the exact feature set around refresh tokens for vNext of IdentityServer – feel free to chime if you have additional ideas or input.