Skip to content

Send Basic auth on firm OAuth token requests only when staging requires it#257

Open
Benjvandam wants to merge 2 commits into
mainfrom
fix-staging-oauth-basic-auth
Open

Send Basic auth on firm OAuth token requests only when staging requires it#257
Benjvandam wants to merge 2 commits into
mainfrom
fix-staging-oauth-basic-auth

Conversation

@Benjvandam

Copy link
Copy Markdown
Contributor

Problem

On staging, the CLI attaches the SF_BASIC_AUTH HTTP Basic header to every firm request, including the OAuth /token POSTs (initial authorize, manual refreshFirm, and the automatic 401-refresh interceptor).

When a staging environment has its Basic Auth gateway disabled (e.g. set up with the "disable HTTP basic auth" option), that header breaks authentication: Doorkeeper treats the gateway credentials as the OAuth client, finds no matching client, and responds 401 "Client authentication failed ... unknown client". As a result silverfin authorize and token refresh fail on such stagings.

Confirmed empirically: the same /oauth/token request, with the same client_id/client_secret, succeeds when the Basic header is omitted.

Fix

Add AxiosFactory.createTokenInstanceForFirm(), which builds the token-endpoint axios instance and attaches the Basic header only when the staging gateway actually requires it — detected by a one-time, per-host probe for a WWW-Authenticate: Basic challenge (an unauthenticated request to a Basic-protected host returns 401 WWW-Authenticate: Basic).

  • Gateway disabled → no Basic header → token request authenticates via client_id/client_secret, as Doorkeeper expects.
  • Gateway enabled → Basic header attached, so the request still passes the gateway.
  • Production → never probed; the header is never added (behaviour unchanged).

All three firm /oauth/token paths now route through this method (initial authorize, refreshFirm, and the 401-refresh interceptor). Staging data calls keep the Basic header; production behaviour is unchanged.

Tests

Updated tests/lib/api/axiosFactory.test.js and tests/lib/api/silverfinAuthorizer.test.js, including new coverage for gateway-present vs gateway-disabled staging. All api-layer tests pass (91/91).

…es it

silverfin-cli attaches the SF_BASIC_AUTH header to every staging request, including the OAuth /token POSTs. On a staging environment whose Basic Auth gateway is disabled, that header makes Doorkeeper read the gateway credentials as the OAuth client and reject the request with "unknown client" - breaking `silverfin authorize` and token refresh.

Add AxiosFactory.createTokenInstanceForFirm, which probes the host once for a WWW-Authenticate: Basic challenge and attaches the Basic header only when the gateway actually requires it. Route all three firm /oauth/token requests through it: initial authorize, manual refreshFirm, and the automatic 401-refresh interceptor. Staging data calls keep the Basic header; production is unchanged.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f32bca5-7eea-450f-bb4a-360f5de3e627

📥 Commits

Reviewing files that changed from the base of the PR and between cf51f23 and 86cff50.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • CHANGELOG.md
  • lib/api/axiosFactory.js
  • package.json
  • tests/TESTS.md
  • tests/lib/api/axiosFactory.test.js
  • tests/lib/api/silverfinAuthorizer.test.js
✅ Files skipped from review due to trivial changes (3)
  • package.json
  • CHANGELOG.md
  • tests/TESTS.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/lib/api/silverfinAuthorizer.test.js
  • lib/api/axiosFactory.js

Walkthrough

The firm OAuth/token request path now uses a token-only axios client that conditionally adds Basic auth for staging after a host probe. SilverfinAuthorizer now awaits that client for refresh and token retrieval, and the related tests and release metadata were updated.

Changes

Firm token client flow

Layer / File(s) Summary
Token-only factory and staging probe
lib/api/axiosFactory.js, tests/lib/api/axiosFactory.test.js, tests/TESTS.md, CHANGELOG.md, package.json, tests/lib/api/silverfinAuthorizer.test.js
Adds createTokenInstanceForFirm, caches staging Basic-auth gateway detection by host, routes the factory refresh request through the new helper, and updates the factory tests, documented test cases, and release metadata for the new token-instance behavior.
Authorizer token request wiring
lib/api/silverfinAuthorizer.js
refreshFirm and #getFirmAccessToken now await createTokenInstanceForFirm before posting refresh and OAuth token requests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the problem, fix, and tests, but it omits the template's Fixes #, Testing Instructions, and checklist sections. Add the missing template sections: Fixes #, step-by-step Testing Instructions, and the Author/Reviewer checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: conditional Basic auth for firm OAuth token requests on staging.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-staging-oauth-basic-auth

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/api/axiosFactory.js`:
- Around line 84-94: The basic-gate probe in the host-checking logic should not
cache a false “gateway disabled” result when the request fails ambiguously
without a response. Update the probe around the axios.get call in the basic-gate
method so that only a confirmed non-Basic response sets and stores false, while
timeout/DNS/TLS-style failures leave the host uncached or return an
indeterminate state. Use the existing `#basicGateByHost` cache and the current
response/error header inspection to distinguish confirmed Basic challenges from
probe failures before writing to the cache.

In `@tests/lib/api/silverfinAuthorizer.test.js`:
- Line 79: The mock for createTokenInstanceForFirm in SilverfinAuthorizer tests
is synchronous even though the real method returns a Promise, so it can hide
missing await bugs. Update the test setup around
AxiosFactory.createTokenInstanceForFirm to return an async-resolving mock (using
the same mockAxiosInstance) so SilverfinAuthorizer is exercised with the real
async behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64551d47-8a64-47b7-9747-82d9c47ec0f1

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdff2d and cf51f23.

📒 Files selected for processing (4)
  • lib/api/axiosFactory.js
  • lib/api/silverfinAuthorizer.js
  • tests/lib/api/axiosFactory.test.js
  • tests/lib/api/silverfinAuthorizer.test.js

Comment thread lib/api/axiosFactory.js
Comment thread tests/lib/api/silverfinAuthorizer.test.js Outdated

@michieldegezelle michieldegezelle left a comment

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.

🟠 Major — check-cli-version CI is failing: package.json / package-lock.json are still at 1.56.1 (same as main), so the version-bump step fails before the changelog check runs. Bump the version (e.g. 1.56.2 for a bugfix) and add a matching ## [1.56.2] entry to CHANGELOG.md describing the conditional staging Basic-auth behaviour — unless the PR description checkbox to skip the version check is intentional.

💡 Suggestion — tests/TESTS.md is stale: lines 360–362 still document AxiosFactory.createAuthInstanceForFirm and the old single “always Basic on staging” behaviour; update to createTokenInstanceForFirm and the gateway-probe cases added in this PR.

- axiosFactory: re-throw (instead of caching) on a transport-level Basic-gate
  probe failure with no HTTP response, so one transient blip can't suppress the
  Basic header for the rest of the process and break every following token
  exchange on a gated staging; add a regression test that asserts the next call
  re-probes.
- silverfinAuthorizer test: mock createTokenInstanceForFirm with mockResolvedValue
  (the method is async) so a dropped await would be caught.
- Bump version 1.56.1 -> 1.56.2 and add a CHANGELOG entry (fixes check-cli-version CI).
- TESTS.md: replace stale createAuthInstanceForFirm rows with the current
  createTokenInstanceForFirm tests, including the gateway-probe cases.
@Benjvandam

Benjvandam commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@michieldegezelle both done in 86cff50 — updated to 1.56.2 with a changelog entry, and updated the stale TESTS.md rows to createTokenInstanceForFirm.


it("should not thrown an error for missing tokens", () => {
firmCredentials.getHost.mockReturnValue(mockHost);
describe("Create token instance for firm", () => {

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.

💡 Suggestion: The static AxiosFactory.#basicGateByHost cache persists across all test cases in this file — jest.clearAllMocks() in beforeEach doesn't reset static class fields. These tests only stay isolated because each uses a distinct hostname (gated./open./flaky.staging…). If a future staging token-instance test reuses an existing host (e.g. the widely-used https://test-api.staging.getsilverfin.com), it'll silently false-pass/fail depending on run order, and the toHaveBeenCalledTimes(2) assertion is similarly run-order sensitive. Consider a test-only reset to make isolation explicit rather than relying on unique hostnames — e.g. expose static _resetBasicGateCache() { this.#basicGateByHost = {}; } and call it in beforeEach.

@michieldegezelle michieldegezelle left a comment

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.

Claude made 1 more comment, but approved already

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