Skip to content

Token refresh#5

Merged
EthanHeilman merged 10 commits intomainfrom
refreshflow
Apr 13, 2026
Merged

Token refresh#5
EthanHeilman merged 10 commits intomainfrom
refreshflow

Conversation

@EthanHeilman
Copy link
Copy Markdown
Collaborator

@EthanHeilman EthanHeilman commented Nov 2, 2025

This PR adds a short section on Token Refresh

  • non-normative examples

Implementation: openpubkey/openpubkey#354

Comparison with RFC 9449 Refresh Flow

client type key binding RFC 9449
public Refresh DPoP constrained Refresh DPoP constrained
confidential Refresh DPoP constrained Refresh DPoP NOT constrained

RFC 9449 OAuth 2.0 DPoP Refresh flow defines the refresh flow as follows:

The Refresh Flow must contain a DPoP signed by a public key. What this public key is associated with depends on the if the request is made by a public or confidential client.

Refreshing an access token is a token request using the refresh_token grant type made to the authorization server's token endpoint. As with all access token requests, the client makes it a DPoP request by including a DPoP proof.

For public clients the DPoP must verify for the public key bound to the refresh token used in the refresh request and this public key must be the can public key used to obtain the refresh token originally, i.e. it can not change without requesting a new refresh token.

When an authorization server supporting DPoP issues a refresh token to a public client that presents a valid DPoP proof at the token endpoint, the refresh token MUST be bound to the respective public key. The binding MUST be validated when the refresh token is later presented to get new access tokens. As a result, such a client MUST present a DPoP proof for the same key that was used to obtain the refresh token each time that refresh token is used to obtain a new access token.

For confidential clients refresh tokens are not bound to a public key, but a DPoP is sent anyways. It is implied by the fact that a Refresh Request is a type of Access Token request, that the public key in the DPoP supplied becomes the new public key in the refresh Access Token. This would enable rotating the public key in the access token.

Refresh tokens issued to confidential clients (those having established authentication credentials with the authorization server) are not bound to the DPoP proof public key because they are already sender-constrained with a different existing mechanism. [...] This existing sender-constraining mechanism is more flexible (e.g., it allows credential rotation for the client without invalidating refresh tokens) than binding directly to a particular public key. [...] As a result, such refresh tokens are sender-constrained by way of the client identifier and the associated authentication requirement. This existing sender-constraining mechanism is more flexible (e.g., it allows credential rotation for the client without invalidating refresh tokens) than binding directly to a particular public key.

For public clients, the refresh flow is the same for OpenID Connect Key Binding and RFC 9449.

For confidential clients there is one difference between the refresh flow for OpenID Connect Key Binding 1.0 and RFC 9449. RFC 9449 appears to allow the Refresh Token to change the Access Token's public key during Refresh. OpenID Connect Key Binding requires the same public key be used. That is, OpenID Connect Key Binding keeps the same behavior across public and confidential clients. In essence RFC 9449 allows the credentials of the Refresh Token, in confidential clients, to override the public key associated with the session, whereas OpenID Connect Key Binding sees the public key as essential to the session.

The reasoning for this difference:

The credentials for client are universal to that client. For instance if there are 1,000 active sessions that need to be refreshed, each session must be able to make refresh requests using those credentials. The public key, on the other hand, is specific to a session. If we allow the client credentials to override the public key than any of these sessions could compromise any other session, by using the refresh flow to inject a attacker controlled public key.

Instead if the public key can not be overridden, then each session could use a different HSM for that key pair. If Alice's session on the backend is compromised, she could not hijack Bob's session, because she could not replace Bob's public key via a token refresh. More importantly, if an attacker were to compromise the client credentials, the attacker would not be able to break the security of any of the existing sessions without first also compromising the key pairs associated with those sessions. This allows implementors in complex applications to significant limit the blast radius of a compromise.

Finally client credentials are often just a bearer secret that is sent over the wire to the OP/AS. This dramatically increases the difficulty of securing them. Having a key pair be the foundation of a sessions security provides a firm foundation. Given that protocol already supports a key pair and treats the key pair in this way for public clients, it is valuable to use this mechanism to upgrade confidential clients to the security offered by public clients.

Comment thread openid-connect-key-binding-1_0.md
@EthanHeilman EthanHeilman changed the title Token refresh, minor fixes Token refresh Nov 9, 2025
@JonasPrimbs
Copy link
Copy Markdown
Contributor

To the best of my understanding, refreshing ID Tokens should not alter the ID Token's DPoP-bound JWK.

I like the idea of allowing the RP also to refresh the JWK. Do you have any security concerns, or is there a reason it should be prevented from doing that?

@dickhardt
Copy link
Copy Markdown
Collaborator

To the best of my understanding, refreshing ID Tokens should not alter the ID Token's DPoP-bound JWK.

I like the idea of allowing the RP also to refresh the JWK. Do you have any security concerns, or is there a reason it should be prevented from doing that?

@JonasPrimbs How are you thinking the RP would refresh the JWK?

@JonasPrimbs
Copy link
Copy Markdown
Contributor

@JonasPrimbs How are you thinking the RP would refresh the JWK?

@dickhardt By sending another token request with authorization_grant=refresh_token (classic token refresh) and using a new JWK in the DPoP header.

@EthanHeilman
Copy link
Copy Markdown
Collaborator Author

@JonasPrimbs
I'm currently leaning towards not allowing the refresh flow to alter the JWK for following reason. I would really appreciate counter-arguments. I want to get it right.

There seem to be two ways to rotate JWKs during refresh.

  • First, you could simply introduce a new JWK key2 and perform a DPoP over key2 along with the refresh token. This would significantly weaken security because an attacker that steals the refresh token could now switch to a JWK of their choice.
  • Second, you could introduce a new JWK key2 and perform a DPoP over key1 and key2 along with the refresh token. This second option is significantly stronger as it proves the RP is holding both key1 and key2.

The second approach, while stronger than the first approach, is still to my mind weaker than the not allowing key rotation during refresh. Below I will explain my thinking on problems with the second approach.

If you are protecting the JWK signing key, key1, as a non-extractable key in a web browser, each time you change the key you introduce the risk that the new JWK signing key, key2, isn't setup to be non-extractable.

Consider the following attack:

  1. Alice generates key1 as a non-extractable key in her browser.

  2. Eve compromises Alice's javascript client via a XSS attack and can now request signatures from Alice's non-extractable key1, but can not extract/export key1. This means Eve can only impersonate Alice while Alice has the browser tab with the compromised javascript open. If Alice closes their tab, turns off her computer or refreshes, in most scenarios Eve loses access.

  3. IF we allow the ability to rotate JWKs, Eve generates a new key, key2 which is extractable and then does a DPoP Refresh flow with the refresh token and key1, to move to key2. Now Eve can use key2 and the refresh token to impersonate Alice indefinitely.

However if we do not allow the ability to rotate JWKs, Eve can not use the refresh flow to substitute an extractable key2 for Alice's original key1 which is non-extractable. This also applies to the use of HSM/passkeys in other settings. Remote attestation can be used to prove a key was generated inside an HSM, but this attacks complexity and additional security assumptions.

Normally we rotate keys to minimize the window of vulnerability after a compromise. I could be mistaken and I'm very interested in hearing disagreeing opinions, but in this case, I don't only see ways in which enabling key rotations in the refresh flow helps an attacker.

Appendix H Device Signing Keys and XSS Attacks in the OpenPubkey paper, has a deeper discussion of how to use JWKs with extractable keys to constraint XSS attackers. It isn't necessary for this discussion, but does provide more context.

@JonasPrimbs
Copy link
Copy Markdown
Contributor

@EthanHeilman
I agree. Providing the opportunity to refresh the key allows cloning the session to a remote attacker, while fixing this key enables the RP to protect itself from this attack by using a non-extractable key.

Besides that, I'm not feeling well with not being able to rotate the key of a refresh token. However, specifically for the refresh token, I think this is fine if we use hardware-bound/non-extractable tokens (TPM- or HSM-backed). The tokens themselves can be rotated or revoked in the event of a compromised RP being detected, and I value the security advantage of not being able to move the refresh token to another device.

Maybe combining the following requirements can reduce this problem to an acceptable level:

  1. Binding the refresh token to a fixed non-extractable key.
  2. Allowing the RP to refresh the ID token with another key (explicitly NOT the refresh token key) by proving the possession of both keys (refresh token key to prove authorization + ID token key to prove possession).
  3. Short ID token lifetime (~1 minute).

Since the attacker would need the refresh token key to obtain a new ID token, the user must be online, as the refresh token key is non-extractable and can therefore not be transferred to the attacker's device.
The attacker could still use XSS to get a new ID token bound to the attacker's extractable key pair. However, the short validity period makes this ID token usable only within its lifetime. To obtain a new one, the attacker again requires an online user with an active XSS attack and cannot refresh the ID token from their device indefinitely.

Using separate key pairs for distinct use cases with different lifetimes (long-term refresh tokens for refreshing access tokens, short-term ID tokens for authentication, and short-term access tokens for authorization) is a nice pattern that should also be applied here, if we want to do everything right.
I have just discovered that there is an Internet Draft with the same purpose, but for access tokens (rotating short-term access token key pairs and fixing the long-term refresh token).
They use a dedicated DPoP-RT header for the DPoP proof of the refresh token. Perhaps we could also introduce a DPoP-ID header for this purpose.

@EthanHeilman
Copy link
Copy Markdown
Collaborator Author

@JonasPrimbs

Besides that, I'm not feeling well with not being able to rotate the key of a refresh token. However, specifically for the refresh token, I think this is fine if we use hardware-bound/non-extractable tokens (TPM- or HSM-backed).

Couldn't you force rotation by simply rejecting the refresh token so the user has to log in again and generate a new key pair?

I can see RP wanting the ability in some circumstances to bind tokens (ID Tokens, Refresh Tokens) to non-extractables keys, but I don't see a good mechanism to enforce it at the OP or RP Consuming Component (RPCC). The RPs best bet is to simply writing their RP Authenticating Component (RPAC) to use an non-extractable key.

Since the attacker would need the refresh token key to obtain a new ID token, the user must be online, as the refresh token key is non-extractable and can therefore not be transferred to the attacker's device. The attacker could still use XSS to get a new ID token bound to the attacker's extractable key pair. However, the short validity period makes this ID token usable only within its lifetime. To obtain a new one, the attacker again requires an online user with an active XSS attack and cannot refresh the ID token from their device indefinitely.

What you propose here works, but I don't see the advantage over just not allowing the key to be rotated.

ID Token key can't be rotated (non-extractable keys used) --> XSS attacker locked out immediately on tab close

vs.

ID Token key rotation via refresh (ID Token 1 min expiration) --> XSS attacker locked out 1 minute after tab close.

I have just discovered that there is an Internet Draft with the same purpose, but for access tokens (rotating short-term access token key pairs and fixing the long-term refresh token).

Thanks for sharing this. I like their idea of separating key pairs for the access token and refresh token because access tokens key pairs are shared with less trusted components whereas refresh tokens should be better protected.

For this to improve security for a use case, I think two things need to be true for that use case:

  1. The Access Token and Refresh Token key pairs must stored on different components with different levels of trust.
  2. The Access Token can't be stored as non-extractable.

As noted in the RFC:

"large, distributed systems with many worker nodes, however, involving an HSM in every transaction is operationally impractical, motivating this extension."

So the Access Token key pair isn't non-extractable and it can be sent around to worker nodes as needed. It starts on the same component as Refresh Token so that the initiate two DPoP headers can be generated and then it gets broadcast to worker nodes.

At least the ways I am familiar with using ID Tokens, the refresh token and the ID Token are stored in the same component and you'd make both non-extractable. That said I live very much in the public client side of OpenID Connect and might be missing conventional client use cases. What ID Token use cases are thinking about here?

The mechanism of DPoP-RT is clever since it merely extends existing behavior. Our Key-Binding spec as currently written would be forwards compatible with this:

"If the DPoP-RT header is omitted, the AS follows the behavior defined in [DPoP] and binds both tokens to the same key from the DPoP proof." Separating DPoP Bindings for Access and Refresh Tokens

Copy link
Copy Markdown
Collaborator

@dickhardt dickhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Copy Markdown
Contributor

@JonasPrimbs JonasPrimbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! As commented, I only recommend adding a reference to Section 5 of the DPoP RFC to clarify details on the Refresh Token issuance.


- its `cnf` claim MUST be the same as in the ID Token issued when the original authentication occurred.

If a new Refresh Token is returned as a result of a Refresh Request, the newly issued Refresh Token MUST continue to be bound to the same public key as the original Refresh Token.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reference Section 5 of the DPoP RFC 9449 here, which describes detailed information on DPoP Refresh Token issuance: https://datatracker.ietf.org/doc/html/rfc9449#section-5-8

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ref for this

@EthanHeilman
Copy link
Copy Markdown
Collaborator Author

@JonasPrimbs Any additional desired changes or does this pass review?

@EthanHeilman EthanHeilman merged commit 26d5f9f into main Apr 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants