Skip to content

fix: preserve the existing refresh token when a refresh response omits one - #64

Open
nnabeyang wants to merge 3 commits into
germ-network:mainfrom
nnabeyang:fix/preserve-refresh-token-when-omitted
Open

fix: preserve the existing refresh token when a refresh response omits one#64
nnabeyang wants to merge 3 commits into
germ-network:mainfrom
nnabeyang:fix/preserve-refresh-token-when-omitted

Conversation

@nnabeyang

@nnabeyang nnabeyang commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Not every authorization server rotates refresh tokens. RFC 6749 §6 leaves that to the server:

The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token.

RFC 9700 §2.2.2 spells out when rotation is not required at all:

Refresh tokens for public clients MUST be sender-constrained or use refresh token rotation as described in Section 4.14.

A DPoP-bound refresh token satisfies the first branch, so a server issuing one is under no obligation to rotate.

A response that carries no refresh_token therefore leaves the client's existing one in force. OAuth4Swift instead discarded it: after such a refresh the session held no refresh token at all, could never refresh again, and signed the user out as soon as the access token expired. Against a server that does not rotate, that happened on the very first refresh.

The refresh path now keeps the refresh token it presented. A response that does carry a refresh_token still replaces the stored one.

Two details come with keeping it:

  • The preserved token's fetchedOn moves to now. It records when the token was last exchanged, and refresh(debounce:) reads it to decide whether a refresh is due, so leaving it behind would keep the debounce permanently satisfied against a non-rotating server.

  • A refresh_token_timeout that arrives without a refresh_token is applied to the preserved token, per draft-ietf-oauth-refresh-token-expiration, which TokenEndpointResponse already cites for that field:

    The authorization server MAY return these values even if the response contains no refresh_token field in the response, in which case the values correspond to the presented refresh_token.

    The field is a sliding window — "the time in seconds that the refresh token may be held by the client without exchanging" — so ignoring it would expire a token the server still honors. Without the field the existing expiry is left alone.

The first commit adds three regression tests: the token survives a response that omits it, the session can refresh again afterwards, and a refresh_token_timeout without a refresh_token restates the preserved token's expiry. All three fail against the previous implementation. The second commit applies the fix and makes the complete suite pass.

@changeset-bot

changeset-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4d5f120

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@germ-network/oauth4swift Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nnabeyang
nnabeyang force-pushed the fix/preserve-refresh-token-when-omitted branch from 81976ed to 4d5f120 Compare August 21, 2026 06:03
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.

1 participant