fix(oauth2): RFC-compliant token lifecycle#11
Merged
Conversation
- P1: middleware calls forceRefresh() instead of exchangeCodeForToken() so 401 retries prefer refresh_token grant before falling back to api_token - P2: refreshAccessToken() now sends Authorization: Basic header (RFC 6749 §3.2.1) - P3: proactive expiry buffer raised 60s → 120s to absorb clock skew - P4: separate refreshEndpoint param passed to OAuth2Client constructor - Add thundering-herd deduplication via shared pendingToken promise - Update unit tests: 12 tests covering all fixed behaviours
Upsun SDK checker reportDisplay raw output |
There was a problem hiding this comment.
Pull request overview
This PR updates the SDK’s OAuth2 token acquisition/refresh behavior to align more closely with RFC 6749/6750, including correct 401 retry behavior, refresh-token preference, a larger expiry buffer, optional separate refresh endpoints, and concurrency deduplication to avoid thundering-herd refreshes.
Changes:
- Added
OAuth2Client.forceRefresh()and updated the 401 retry middleware to use it, ensuring refresh-token grants are preferred before falling back toapi_token. - Updated refresh behavior to include
Authorization: Basic ...and increased the proactive refresh buffer (60s → 120s), plus deduped concurrent acquisitions via a shared in-flight promise. - Expanded unit test coverage for the new lifecycle behaviors, including refresh endpoint selection and concurrency deduplication.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/core/oauth-provider.ts |
Implements RFC-aligned token lifecycle logic (force refresh, refresh preference, header parity, buffer increase, refresh endpoint, dedup). |
src/upsun.ts |
Wires refresh_endpoint into OAuth2Client and updates 401 retry middleware to call forceRefresh(). |
tests/unit/core/oauth2-client.test.ts |
Adds unit tests for the updated token lifecycle and concurrency behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
140
to
142
| const data: TokenResponse = await response.json(); | ||
| this.storeTokenData(data); | ||
| } |
| refresh_token: string; | ||
| expires_in: number; // en secondes | ||
| refresh_token?: string; | ||
| expires_in: number; // in secondes |
flovntp
approved these changes
Jun 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes 4 OAuth2 bugs identified in the SDK, bringing the token lifecycle into full compliance with RFC 6749 and RFC 6750.
Changes
P1 — 401 retry middleware bypasses
refresh_token(critical)createAuthRetryMiddleware()was callingexchangeCodeForToken()directly on a 401, always forcing agrant_type=api_tokenexchange and skipping any availablerefresh_token.Fix: replaced with
forceRefresh()— a new public method onOAuth2Clientthat clears cached token state and delegates toensureValidToken(), which prefersrefresh_tokengrant before falling back toapi_token(RFC 6749 Fig. 2, steps F→G→H).P2 —
refreshAccessToken()missingAuthorization: BasicheaderexchangeCodeForToken()correctly sentAuthorization: Basic platform-api-user:butrefreshAccessToken()did not, causing silent failures on auth servers that require it for confidential clients (RFC 6749 §3.2.1).Fix: added the same
Authorization: Basicheader torefreshAccessToken().P3 — Proactive buffer too short (60s → 120s)
Clock skew between client and server could cause the server to consider a token expired before the client's 60s buffer triggered a refresh.
Fix: increased buffer to
120 * 1000ms.P4 —
refresh_endpointconfig field unusedUpsunConfig.refresh_endpointwas defined but never passed toOAuth2Client, so both grant types always hit the same endpoint.Fix: added optional 4th parameter
refreshEndpointtoOAuth2Clientconstructor (defaults totokenEndpoint), wired up fromUpsunClient.Bonus — Thundering-herd protection
Multiple concurrent requests receiving a 401 simultaneously would each call
forceRefresh()independently, issuing N auth requests.Fix:
ensureValidToken()uses a sharedpendingToken: Promise<void> | null— the first caller creates the promise, all concurrent callers await the same one.Tests
12 unit tests in
tests/unit/core/oauth2-client.test.ts(was 4), covering:Authorization: Basicheader on both grant types (P2)grant_type=refresh_tokenbody assertion on expiry (P1)api_tokenwhen refresh failsforceRefresh()re-acquires a server-revoked "valid" tokenforceRefresh()prefersrefresh_tokengrant when availableforceRefresh()deduplication — 1 auth call for 5 concurrent callersrefreshEndpointconstructor param (P4)