I am working on an authentication server that can act as a central place to manage authentication for multiple projects, sort of like keycloak or ory kratos.

While working on implementing refresh_tokens (RT) I got an idea for an alternative solution that would not rely on cookies. But first, what challenges do I see with using refresh_tokens:

Secure Cookies

I am currently storing the RT inside an HttpOnly cookie with the SameSite configuration set to strict to avoid the browser leaking the RT. This also requires me to either 1) have the authorization server run on the same site (e.g. auth.example.com) or 2) proxy the request though my API server to the authentication server.

Refresh Token Rotation

When a client asks for a new access token (AT) the old RT get invalidated and the client receives a new pair of AT and RT, this works well for the happy case where things don't break but IMO creates a bad UX in case something goes wrong e.g. the user is on a bad internet connection and request a new AT, while the server is processing the request the client loses the connection and never receives the new RT, now the user is in an invalid state and needs to sign in again.

A cookieless solution

  1. Generate a Private/Public Key Pair
  2. Alongside the username and password send the generated public key to the authentication server
  3. On success the authentication server returns a session ID that can be used to associate the current session with the public key that was send to the server
  4. The client stores the session ID and the private key in indexddb (set extractable to false)
  5. In order to request a new access token for a given session the client calls the authentication server with a short lived Access Token signed with the private key.

Private Key Storage

In order to prevent private keys from leaking outside the browser context we need to configure the CryptoKey object to not be extractable, this allows us to use the private key to sign the AT without directly accessing the key and preventing XSS.

MDN: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey


I think this approach would have the same security characteristics compared to using refresh tokens but can provide a better UX in case of failure, additional I don't have to run the authentication server on the same site as my client projects.

Am I missing something?

  • You say that in case of bad connection "the client loses the connection and never receives the new RT". But the same is applicable in your case on the step 5. How is your approach different? Why do you think you approach is resistant to bad connections?
    – mentallurg
    Commented Feb 27, 2021 at 23:03
  • My approach would create a "single use" access token on the client and then exchange this AT with new AT from the authentication server that I can then use for the resource server. If the client loses the connection during the exchange and then go's back online you would simple generate a new AT Commented Feb 27, 2021 at 23:19
  • @fragile_frogs Ah, so your public & private key pair are not updated every time you refresh a token? Well, your solution is simply completely different then. It doesn't provide the same guarantees as revoked refresh tokens. Revoking refresh tokens means that an attacker that obtains an old refresh token cannot use it for anything, in your case the attacker can always use the private key to get new tokens. And if you add private key rotation by sending it to the server signed by the old one you end up in the same problem as with refresh tokens, but client -> server this time
    – GACy20
    Commented Dec 20, 2022 at 15:58

1 Answer 1


Well, first of all, the simple approach to fixing the problem you face is to not invalidate the RT immediately upon use. Either just don't re-issue it at all (keep using it until the session ends due to user action, or the token expires), or wait to invalidate the old one until you've confirmed that the user received the new one (e.g. by checking the next request the user sends to the site and seeing if the new token is present. The second approach adds an additional DB access on each refresh (on the subsequent request), but you can safely store the "new token verified" flag on the client (possibly as part of the cookie, which gets updated during the verification) since the whole "make sure the user gets their new token" thing is just a convenience factor for the user anyhow and doesn't impact security.

As for your proposed implementation, it looks like it should work. However, the whole thing is complicated in a way that adds little actual security. You're using asymmetric signatures so that you can hide the private key from JS, which is only relevant in the case of XSS that happens at some point post-login (as most of them do). However, this - like the HttpOnly flag on cookies - is barely relevant security-wise. As long as the victim has any document (including in a window or iframe) open to the vulnerable page, the attacker can remote-control the victim's session and see all data. Assuming the access token is visible to script - which it must be, for a script to sign it for the refresh process - the attacker can use that token themselves in the background, making their own requests and viewing the responses. The attacker won't be able to refresh the AT once the victim closes all vulnerable pages, but realistically that doesn't matter. Either the attacker will have already achieved their goals, or they'll just use the XSS attack again when they need it.

EDIT: One other potential problem with your design: where do the tokens that the client signs (with the private key) come from? If they're generated client-side and not stored server-side to check for reuse, an attacker who gets even one of them can just replay that one forever. Even if they're checked for reuse, an attacker can just generate a few million of them during the active XSS window, and realistically never worry about running out. Embedding a timestamp won't help; the attacker can pre-generate signed tokens for future timestamps.

Instead, the token needs to be either a valid session token[/access token/JWT], or a short-lived nonce (expiration enforced by the server) chosen by the server in response to a client request specifying the public key. The former approach works fine for refreshing an ongoing session, but requires that the session token be visible to script (acceptable, especially if its lifetime is short enough, but you're trying to avoid this...). It also doesn't work for sessions that have expired; you can't use this like a normal refresh token that outlives its session token! The second approach requires additional state be temporarily stored on the server, although not in an especially expensive way (the nonce could be deleted once used, or at least once the resulting new token is used).

  • "If an authorization server does choose to issue refresh tokens to browser-based applications, then it MUST issue a new refresh token with every access token refresh response. Doing this mitigates the risk of a leaked refresh token, as a leaked refresh token can be detected if both the attacker and the legitimate client attempt to use the same refresh token." - ietf Commented Feb 28, 2021 at 0:06
  • "he whole "make sure the user gets their new token" thing is just a convenience factor for the user anyhow and doesn't impact security." That's the whole point of my question, my last sentence states that this won't add any extra security but rather improve the user experience in case something goes wrong. Commented Feb 28, 2021 at 0:08
  • 2
    That whole section is full of unsupported and frankly silly claims. "browser-based applications provide many attack vectors by which a refresh token can be leaked" oh please. Web apps have lots of potential vulnerabilities but token leakage is one of the most trivially solved ones. As for mitigating refresh token leakage, that's both too late to prevent most harm and achievable in other ways. In fact, even their "MUST" only says you MUST issue a new refresh token (which allows detecting replay), not that you need to invalidate the old one (though maybe it makes that requirement elsewhere).
    – CBHacking
    Commented Feb 28, 2021 at 0:41
  • Also, that draft is an old version, "expired" a year ago, and has been updated several times since it was written. Lots of changes to section 8 between what you linked and the current draft, tools.ietf.org/html/… *Edit to previous comment: s/trivially solved/lowest threat/. As your question highlights, it's not always trivial to completely avoid the risk... but the risk you're trying to mitigate is still pretty minor.
    – CBHacking
    Commented Feb 28, 2021 at 0:45
  • Ahh thanks, I didn't see that the draft had expired. Commented Feb 28, 2021 at 11:08

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .