History

test: repro DCR client reuse across a changed redirect URI

Rhys Sullivan committed 3 months ago

Commit ff739201 parent 99c808f1 file · +83 −0Browse files

Unified · Split

packages/core/sdk/src/oauth-register-dynamic.test.ts+83−0
@@ -527,6 +527,89 @@ describe("oauth.registerDynamicClient", () => { · 526 unmodified lines
527 ),527 ),
528 );528 );
529 529
530 // Repro: a recreated sandbox changes the executor's callback origin while
531 // the DCR client minted for the OLD callback survives in storage. Reuse is
532 // keyed on (owner, issuer, resource) and never compares the redirect URI the
533 // client was registered with, so `oauth.start` pairs the stale registration
534 // with the NEW callback — and a strict authorization server (the test AS
535 // included) rejects the authorize request with 400 `redirect_uri is not
536 // registered`. Contract: a changed flow redirect URI must trigger a fresh
537 // registration instead of reusing the stale client.
538 it.effect("re-registers instead of reusing the DCR client when the redirect URI changed", () =>
539 Effect.scoped(
540 Effect.gen(function* () {
541 const server = yield* serveOAuthTestServer({ scopes: ["read"] });
542 const { executor } = yield* makeTestWorkspaceHarness({ plugins });
543 yield* executor.acme.seed();
544 const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
545
546 // Original sandbox: DCR registration carries the ORIGINAL callback.
547 yield* executor.oauth.registerDynamicClient({
548 owner: "org",
549 slug: OAuthClientSlug.make("original-sandbox"),
550 issuer: probe.issuer,
551 registrationEndpoint: probe.registrationEndpoint!,
552 authorizationUrl: probe.authorizationUrl,
553 tokenUrl: probe.tokenUrl,
554 resource: probe.resource,
555 scopes: ["read"],
556 tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
557 clientName: "Acme DCR",
558 redirectUri: FLOW_REDIRECT_URI,
559 originIntegration: INTEG,
560 });
561 yield* server.clearRequests;
562
563 // Recreated sandbox: same persisted oauth_client rows, NEW callback
564 // origin. The register call runs again the way the connect flow does.
565 const recreatedRedirectUri = "https://localhost:6410/api/oauth/callback";
566 const slug = yield* executor.oauth.registerDynamicClient({
567 owner: "org",
568 slug: OAuthClientSlug.make("recreated-sandbox"),
569 issuer: probe.issuer,
570 registrationEndpoint: probe.registrationEndpoint!,
571 authorizationUrl: probe.authorizationUrl,
572 tokenUrl: probe.tokenUrl,
573 resource: probe.resource,
574 scopes: ["read"],
575 tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
576 clientName: "Acme DCR",
577 redirectUri: recreatedRedirectUri,
578 originIntegration: INTEG,
579 });
580
581 // The changed redirect URI must have minted a FRESH registration whose
582 // redirect_uris carry the recreated callback. Today this is 0: the
583 // stale client is reused and the authorize hop below 400s.
584 expect(registerRequestCount(yield* server.requests)).toBe(1);
585
586 const started = yield* executor.oauth.start({
587 owner: "org",
588 client: slug,
589 clientOwner: "org",
590 name: ConnectionName.make("main"),
591 integration: INTEG,
592 template: TEMPLATE,
593 redirectUri: recreatedRedirectUri,
594 });
595 expect(started.status).toBe("redirect");
596 if (started.status !== "redirect") return;
597
598 // The strict AS accepts the authorize request only when the flow's
599 // redirect_uri matches the client's registration — with the stale
600 // reused client this hop is the reported 400.
601 const callback = yield* server.completeAuthorizationCodeFlow({
602 authorizationUrl: started.authorizationUrl,
603 });
604 const connection = yield* executor.oauth.complete({
605 state: started.state,
606 code: callback.code,
607 });
608 expect(String(connection.address)).toBe("tools.acme.org.main");
609 }),
610 ),
611 );
612
530 // Regression: issue #770. Vercel (and other RFC 8252-strict servers) only613 // Regression: issue #770. Vercel (and other RFC 8252-strict servers) only
531 // approve loopback redirect URIs for anonymous DCR, so a hosted/tailnet/LAN614 // approve loopback redirect URIs for anonymous DCR, so a hosted/tailnet/LAN
532 // origin trips `invalid_redirect_uri`. The failure must explain the loopback615 // origin trips `invalid_redirect_uri`. The failure must explain the loopback

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t