Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/oauth-dcr-application-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"executor": patch
---

**Fix: declare the OAuth application type during dynamic client registration**

Dynamic OAuth registrations now identify HTTPS callbacks as web applications
and loopback HTTP callbacks as native applications. This lets strict OAuth
servers validate Executor's redirect URI against the correct client type.
34 changes: 34 additions & 0 deletions packages/core/sdk/src/oauth-register-dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe("oauth.registerDynamicClient", () => {
expect(registerRequest!.body).toContain(FLOW_REDIRECT_URI);
expect(registerRequest!.body).toContain("authorization_code");
expect(registerRequest!.body).toContain("refresh_token");
expect(registerRequest!.body).toContain('"application_type":"web"');
const authorizationRequest = requests.find(
(r) => r.path === "/authorize" && r.method === "GET",
);
Expand All @@ -160,6 +161,39 @@ describe("oauth.registerDynamicClient", () => {
),
);

it.effect("registers loopback callbacks as native applications", () =>
Effect.scoped(
Effect.gen(function* () {
const server = yield* serveOAuthTestServer({ scopes: ["read"] });
const { executor } = yield* makeTestWorkspaceHarness({ plugins });
yield* executor.acme.seed();
const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });

yield* executor.oauth.registerDynamicClient({
owner: "org",
slug: CLIENT,
issuer: probe.issuer,
registrationEndpoint: probe.registrationEndpoint!,
authorizationUrl: probe.authorizationUrl,
tokenUrl: probe.tokenUrl,
resource: probe.resource,
scopes: ["read"],
tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
clientName: "Acme DCR",
redirectUri: "http://127.0.0.1:5394/api/oauth/callback",
originIntegration: INTEG,
});

const requests = yield* server.requests;
const registerRequest = requests.find(
(request) => request.path === "/register" && request.method === "POST",
);
expect(registerRequest).toBeDefined();
expect(registerRequest!.body).toContain('"application_type":"native"');
}),
),
);

it.effect("reuses an existing DCR client for the same owner and authorization server", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/oauth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: authMethod,
application_type: isLoopbackHttpUrl(flowRedirectUri) ? "native" : "web",
scope: input.scopes.length > 0 ? input.scopes.join(" ") : undefined,
},
},
Expand Down