Skip to content

Preserve the bearer token when a refresh yields nothing (1.2.1) - #3

Merged
erelsop merged 1 commit into
mainfrom
fix/bearer-token-preserve
Jul 23, 2026
Merged

Preserve the bearer token when a refresh yields nothing (1.2.1)#3
erelsop merged 1 commit into
mainfrom
fix/bearer-token-preserve

Conversation

@erelsop

@erelsop erelsop commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Bugfix (1.2.1) for a token-clobbering cascade in BearerTokenAuth, surfaced by a real audit run against a live Keap sandbox.

The bug

On a 401/403, onAuthError invokes the refresh callback and then persists the result:

await this.store.setToken(newToken ?? null);

When the refresh returns null — no refresh configured, or an error a refresh can't fix, such as a permission 403 — this overwrites the still-valid token with null. From then on onRequest finds no token and sends every request with no Authorization header, so unrelated later requests fail with 401.

Impact: a single permission-denied resource strips auth from every subsequent request. In the audit that surfaced this, one 403 on /campaigns cascaded into misleading empty 401s on /users, /products, /orders, and /subscriptions — hiding each resource's real status and message.

Reproduction

A 403 on the first request, then a second request to a healthy endpoint:

campaigns: 403 "Access is denied"
users:     401 ""
Authorization header seen per request: [ 'Bearer VALID_TOKEN', '(none)' ]

The second request went out unauthenticated.

Fix

Only persist a successful refresh; leave the stored token untouched otherwise:

if (newToken) await this.store.setToken(newToken);

The failing request still gives up and surfaces its original error (unchanged), but the valid token survives for later requests, so each resource reports its own accurate status.

Tests

Adds a regression test: a null refresh result preserves the stored token, onAuthError does not retry, and onRequest still attaches the original token. Full suite: 262 passing.

BearerTokenAuth.onAuthError called store.setToken(newToken ?? null) after
invoking the refresh callback, so a refresh that returned null -- no refresh
configured, or an error a refresh cannot fix such as a permission 403 --
overwrote the still-valid token with null. Every subsequent request then went
out with no Authorization header and failed with 401, turning one 401/403 into
a cascade of unauthenticated failures across unrelated requests.

Now the stored token is only replaced when the refresh actually produces a new
one; otherwise it is left intact and the failing request surfaces its original
error. Regression test asserts a null refresh preserves the token and that
onRequest still attaches it. Full suite: 262 passing.
@erelsop
erelsop merged commit d98c5e2 into main Jul 23, 2026
8 checks 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.

2 participants