fix(oauth): keep the scope filter from overwriting persisted storage

Copilot review round 5 on #2114 — a hole in last round's fix.

When the persisted scope was `offline_access` alone,
`scopeForDeclinedRefreshGrant` returned undefined.
`OAuthManager.createOAuthProvider` reads that as "nothing stored" and
seeds storage with the configured scope, so the request-only filter
silently overwrote the value it promised to leave alone — and
re-enabling the grant could no longer restore it.

It now falls back to the configured scope, which is both the right
thing to request and inert to that branch. The previous "storage is
never rewritten" test could not see this: it ran at the provider level,
while the seeding branch lives in the manager. The new regression runs
there, and fails when the fallback is removed.

Also corrects the EMA rationale, which was wrong even though its
conclusion held. `createOAuthProviderForTransport` does build a
`BaseOAuthClientProvider` and wrap it, and `EmaTransportOAuthProvider`
forwards `clientMetadata` — so the setting is not bypassed outright.
What it cannot change is the IdP authorization leg's fixed
`openid offline_access`, which is the leg the user signs in through and
therefore the reason the control cannot affect EMA's consent behavior.
Fixed in the docs, the code comment, the UI string and the test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-24 21:48:21 -04:00
commit 1bbdef03eb
2 changed files with 72 additions and 16 deletions

View file

@ -818,9 +818,11 @@ describe("ServerSettingsForm", () => {
).not.toBeInTheDocument();
});
// #2068 — EMA never builds the Inspector's OAuth provider at all; it
// authorizes against the enterprise IdP with a fixed `openid offline_access`.
// The checkbox therefore cannot affect that request and must say so.
// #2068 — EMA wraps this provider and forwards its `clientMetadata`, so the
// setting is not ignored outright; what it cannot change is the IdP
// authorization leg's fixed `openid offline_access` scope. That leg is the
// one the user signs in through, so the checkbox cannot affect EMA's consent
// behavior and must say so.
it("says the refresh-token setting is not applied under EMA", () => {
const { rerender } = renderWithMantine(
<ServerSettingsForm
@ -830,7 +832,7 @@ describe("ServerSettingsForm", () => {
/>,
);
expect(
screen.getByText(/which this setting does not reach/),
screen.getByText(/which this setting cannot change/),
).toBeInTheDocument();
rerender(
@ -841,7 +843,7 @@ describe("ServerSettingsForm", () => {
/>,
);
expect(
screen.queryByText(/which this setting does not reach/),
screen.queryByText(/which this setting cannot change/),
).not.toBeInTheDocument();
});

View file

@ -539,13 +539,15 @@ export function ServerSettingsForm({
.includes("offline_access");
const refreshTokenOptedOut = settings.oauthRequestRefreshToken === false;
// #2068 — suppressed under EMA for a different reason than the endpoint
// overrides: that leg never builds a BaseOAuthClientProvider at all. It
// authorizes against the enterprise IdP with its own fixed scope, so this
// checkbox cannot influence the request. Say so rather than leaving a control
// that looks effective, matching how the overrides annotate themselves.
// #2068 — annotated under EMA. Not because the provider is bypassed (EMA
// wraps a BaseOAuthClientProvider and forwards its `clientMetadata`), but
// because the IdP authorization leg — the one the user signs in through —
// uses a fixed `openid offline_access` scope this setting cannot change. The
// control therefore cannot affect EMA's consent behavior, which is the whole
// reason to touch it. Say so rather than leave it looking effective, matching
// how the endpoint overrides annotate themselves.
const refreshTokenEmaNote = settings.enterpriseManaged
? " Not applied while Enterprise-managed authorization is on: that flow authorizes against the enterprise IdP with its own scope, which this setting does not reach."
? " Not applied while Enterprise-managed authorization is on: that flow signs in against the enterprise IdP using its own fixed scope, which this setting cannot change."
: "";
const rejectedParamKeys = authorizationParams