Skip to content

fix(auth): accept refresh responses without refresh_token - #5

Merged
eluhr merged 1 commit into
masterfrom
fix/refresh-without-rotation
Oct 1, 2026
Merged

eluhr merged 1 commit into
masterfrom
fix/refresh-without-rotation

Conversation

@eluhr

@eluhr eluhr commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Every token refresh fails with BC4 token exchange failed (status=200) as soon as the first access token has expired. Launchpad answers the legacy type=refresh request with only an access_token, but OAuth2Authentication::exchangeRefreshToken() also required refresh_token and expires_in. An integration therefore stopped working two weeks after the initial authorization and needed a manual re-authorization each time.

The response shape matches Basecamp's own SDK: its test case "legacy format refresh" in basecamp/basecamp-sdk returns {"access_token": "..."} only, and the Python SDK reads refresh_token and expires_in as optional fields.

Change

  • Only access_token is required in the refresh response.
  • Without a rotated refresh_token, the current one is kept and passed back to the storage, so TokenStorageInterface stays unchanged.
  • Without expires_in, the documented two-week lifetime (1209600 s) is assumed.
  • Class docblock updated: rotation happens only when Launchpad sends a new refresh token.

Tests

  • testRefreshWithoutRotationKeepsCurrentRefreshToken: access-token-only response keeps the current refresh token and defaults expires_in.
  • testRefreshResponseWithoutAccessTokenFails: a response without access_token still fails.
  • docker compose run --rm php: 23 tests, 57 assertions, all green.

🤖 Generated with Claude Code

Launchpad's legacy `type=refresh` response carries only `access_token`
(see the "legacy format refresh" case in basecamp/basecamp-sdk). The
client required `refresh_token` and `expires_in` as well, so every refresh
failed with "BC4 token exchange failed (status=200)" once the first access
token expired, and the integration needed a manual re-authorization every
two weeks.

Only `access_token` is required now. Without a rotated refresh_token the
current one is kept; without `expires_in` the documented two-week
lifetime is assumed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eluhr
eluhr merged commit dd9a9ae into master Oct 1, 2026
1 check passed
@eluhr
eluhr deleted the fix/refresh-without-rotation branch October 1, 2026 11: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.

1 participant