OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1694)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1625) Re-key the persisted OAuth store to (server, issuer) and add the SEP-837/2350/2352 authorization-hardening UX. The SDK v2 auth() drives the SEP-2352 flow off the provider's ctx.issuer; the Inspector's job is to honor it and round-trip the issuer stamp. SEP-2352 — issuer-bound credentials - ServerOAuthState nests issuer-bound credentials under byIssuer[issuer] = { clientInformation, clientRegistrationKind, tokens }, with an activeIssuer pointer answering the transport's ctx-less per-request bearer read. - Fix a latent bug: OAuthStorageBase re-parsed tokens/client-info through OAuthTokensSchema/OAuthClientInformationSchema, which strip the non-RFC `issuer` field and silently defeat the SDK's discardIfIssuerMismatch. The store re-attaches the stamp from the byIssuer key. - BaseOAuthClientProvider threads ctx.issuer and implements discoveryState()/saveDiscoveryState() (activates the SDK's callback-leg AuthorizationServerMismatchError binding check) and invalidateCredentials(). - Lazy, migration-free upgrade: a legacy pre-1625 blob's top-level clientInformation/tokens are returned as an unkeyed fallback until the first issuer-stamped save promotes them into byIssuer. SEP-837 — application_type - DCR declares application_type: "native" (localhost Inspector). - ClientSettingsForm renders RegistrationRejectedError detail and reflects DCR's deprecated-in-favor-of-CIMD (SEP-991) status. SEP-2350 — step-up scope union - Per-server onInsufficientScope (reauthorize | throw) setting wired ServerSettingsForm -> StoredMCPServer.oauth -> StreamableHTTPClientTransport. - StepUpAuthModal visualizes the prior-union-challenged scope set, tagging each scope `new` vs `already granted`. mcpAuth simplification - Delete the hand-rolled authorizeWithoutRefresh (mishandled issuer stamping); forward forceReauthorization/skipIssuerMetadataValidation to native SDK auth() so both the initial and step-up paths get SEP-2352 stamping. Vocabulary + docs - OAuthStep gains cimd_fetch / issuer_comparison / scope_step_up (recorded on a step-up redirect); the Network tab labels auth requests by OAuth phase. - v2_auth_hardening.md gains an as-built status section. Deferred to the era-aware card: the SdkError(EraNegotiationFailed) -> UnauthorizedError unwrap and finishAuth(URLSearchParams) form (only fire under non-legacy negotiation). Closes #1625 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address @claude review: issuer-stamp source gating + clear/activeIssuer + phase over-match - SEP-2352: stamp the issuer only when the credential actually came from the byIssuer slot (`fromSlot`), not merely when a slot exists. A partially-populated slot alongside a surviving legacy field would otherwise stamp a legacy (possibly cross-AS) credential with the current issuer, defeating discardIfIssuerMismatch. - Issuer-scoped clears no longer promote the cleared issuer to `activeIssuer` (updateIssuerSlot gains a `setActive` flag; save paths keep the promotion). - oauthNetworkPhase matches register/token/authorize as the final path segment (trailing slash tolerated) so `/token/refresh` or `/api/register/foo` no longer over-match. - Tests for all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f915889fc4
commit
7fecebed5e
2 changed files with 1620 additions and 601 deletions
|
|
@ -289,4 +289,24 @@ describe("NetworkEntry", () => {
|
|||
screen.queryByRole("button", { name: "Reveal secrets in body" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels an auth-category request with its OAuth flow phase", () => {
|
||||
const tokenRequest: FetchRequestEntry = {
|
||||
...baseEntry,
|
||||
url: "https://as.example.com/oauth/token",
|
||||
category: "auth",
|
||||
};
|
||||
renderWithMantine(
|
||||
<NetworkEntry entry={tokenRequest} isListExpanded={false} />,
|
||||
);
|
||||
expect(screen.getByText("Token")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not label transport requests or unrecognized auth URLs", () => {
|
||||
renderWithMantine(
|
||||
<NetworkEntry entry={baseEntry} isListExpanded={false} />,
|
||||
);
|
||||
expect(screen.queryByText("Token")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Discovery")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
|
|||
import { MethodBadge } from "../../elements/MethodBadge/MethodBadge";
|
||||
import { CategoryBadge } from "../../elements/CategoryBadge/CategoryBadge";
|
||||
import { maskSecretsInBody } from "../../../utils/maskSecrets";
|
||||
import {
|
||||
oauthNetworkPhase,
|
||||
oauthNetworkPhaseLabel,
|
||||
} from "../../../utils/oauthNetworkPhase";
|
||||
|
||||
export interface NetworkEntryProps {
|
||||
entry: FetchRequestEntry;
|
||||
|
|
@ -247,6 +251,16 @@ export function NetworkEntry({
|
|||
setIsExpanded(isListExpanded);
|
||||
}, [isListExpanded]);
|
||||
|
||||
// OAuth flow phase for `auth`-category requests (discovery / registration /
|
||||
// authorize / token), so the Network tab labels the auth conversation.
|
||||
const oauthPhase =
|
||||
entry.category === "auth" ? oauthNetworkPhase(entry.url) : undefined;
|
||||
const phaseBadge = oauthPhase ? (
|
||||
<Badge color="violet" variant="light">
|
||||
{oauthNetworkPhaseLabel(oauthPhase)}
|
||||
</Badge>
|
||||
) : null;
|
||||
|
||||
const metaBadges = (
|
||||
<>
|
||||
{entry.duration != null && (
|
||||
|
|
@ -278,6 +292,7 @@ export function NetworkEntry({
|
|||
</TimestampText>
|
||||
<MethodBadge method={entry.method} />
|
||||
<CategoryBadge category={entry.category} />
|
||||
{phaseBadge}
|
||||
</HeaderCluster>
|
||||
<ControlsCluster>{metaBadges}</ControlsCluster>
|
||||
</HeaderRow>
|
||||
|
|
@ -306,6 +321,7 @@ export function NetworkEntry({
|
|||
</TimestampText>
|
||||
<MethodBadge method={entry.method} />
|
||||
<CategoryBadge category={entry.category} />
|
||||
{phaseBadge}
|
||||
<CopyButton value={entry.url} />
|
||||
<UrlText>{entry.url}</UrlText>
|
||||
</Group>
|
||||
|
|
|
|||
Loading…
Reference in a new issue