Skip to content

fix(apollo)!: let a refresh through with an expired access token - #251

Merged
BramKaashoek merged 1 commit into
mainfrom
fix/refresh-with-expired-access-token
Oct 8, 2026
Merged

BramKaashoek merged 1 commit into
mainfrom
fix/refresh-with-expired-access-token

Conversation

@BramKaashoek

@BramKaashoek BramKaashoek commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Problem

GatewayAuthPlugin.requestDidStart answers any request that carries an expired access token (or an expired data token) with deleteAccessToken + 401, and it does this before the refresh token is loaded. The refresh request is no exception, so a refresh sent while the expired access cookie is still present fails. Only a retry without the access token succeeds. Consumers that treat a failed refresh as a logout sign the user out.

Change

The refresh token is now loaded before deciding on an expired access or data token:

  • The refresh JWT loads: the expired token is cleared, and the request continues without it.
  • The refresh token is missing or invalid: the request gets the expired 401, as before.

The two invalid-token branches now share a rejectInvalidToken helper. Their behaviour and log fields are unchanged.

A side effect is that a CustomerLogout-style mutation sent with an expired access token now runs and revokes the refresh token. Before, it got a 401.

hive-router-federated-token behaves similarly (src/plugin.rs, if refreshing), with these differences:

  • it only checks that a refresh token is present, not that it is valid;
  • while refreshing, it also ignores invalid access and data tokens;
  • it rejects an invalid refresh token outright.

Breaking (major)

The gateway can't tell which operation is the refresh. Any request that carries a valid refresh token runs without the expired token, which means anonymously if the expired token was the access token. A resolver that mints an anonymous session would then replace the user's refresh token.

  • Clients must send the refresh token with refresh requests only. Use a cookie scoped to a refresh-only refreshTokenPath, or, for header clients, send x-refresh-token on the refresh mutation alone.
  • An expired access token together with an invalid data token now returns INVALID_TOKEN and clears the whole session, including a valid refresh token. Before, it returned the expired 401.
  • An expired access token together with an invalid refresh token still returns the expired 401, but now also clears the refresh cookie.

The changeset and the apollo README document all of this.

Tests

gateway.test.ts has an it.each over the access, data and refresh token states. For each state it asserts the outcome (continues, UNAUTHENTICATED or INVALID_TOKEN) and exactly which cookies are cleared. A mutation sweep covers every guard, every delete call, both listeners and the session wipe.

A successful refresh clears the access cookie and then sets it again in the same response. This was checked over real HTTP:

  • Fastify keys cookies by name, domain and path, so only the set goes out.
  • Express sends both headers in order, so the browser ends up with the new token.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1625914

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

This PR includes changesets to release 1 package
Name Type
@labdigital/federated-token-apollo Major

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

Comment thread packages/apollo/src/gateway.ts Outdated
this.tokenSource.deleteAccessToken(
contextValue.req,
contextValue.res,
if (!(e instanceof TokenExpiredError)) {

@robertmoelker robertmoelker Oct 8, 2026 •

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.

if (e instanceof TokenInvalidError) {
					return this.rejectInvalidToken(
						contextValue,
						"access",
						accessToken,
						e,
					);
				}

@BramKaashoek BramKaashoek changed the title feat(apollo): allow a refresh with an expired access token fix(apollo)!: let a refresh through with an expired access token Oct 8, 2026
@BramKaashoek
BramKaashoek force-pushed the fix/refresh-with-expired-access-token branch from 79a438c to d3dae54 Compare October 8, 2026 12:44
The gateway answered a request carrying an expired access or data token with
a 401 before loading the refresh token, so the first refresh after `exp`
always failed. It now loads the refresh token first and, when it loads,
clears the expired token and continues without it.

BREAKING CHANGE: clients must send the refresh token with refresh requests
only; any other request carrying it runs without the expired token instead
of getting a 401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BramKaashoek
BramKaashoek force-pushed the fix/refresh-with-expired-access-token branch from d3dae54 to 1625914 Compare October 8, 2026 13:33
@BramKaashoek
BramKaashoek merged commit 65767b2 into main Oct 8, 2026
8 checks passed
@BramKaashoek
BramKaashoek deleted the fix/refresh-with-expired-access-token branch October 8, 2026 13:35
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.

2 participants