// Cloud: OAuth as a credential mechanism, over the wire. `probe` discovers an // authorization server's metadata, `createClient` registers an owner-scoped // OAuth app, and the authorization-code flow (`start` → user consent → // `complete`) mints a Connection — every hop real: the typed client drives the // product API while a real OAuth authorization server runs inside the scenario // on 127.0.0.1 (the dev server exchanges the code against it directly). // // Ported from apps/cloud/src/mcp/mcp-oauth.node.test.ts, extended to cover // `complete` (the original stopped at the redirect). import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { composePluginApi } from "@executor-js/api/server"; import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, OAuthClientSlug, OAuthState, } from "@executor-js/sdk/shared"; import { serveOAuthTestServer } from "@executor-js/sdk/testing"; import { scenario } from "../src/scenario"; import { Api, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin()] as const); const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; /** Narrow a `start` result to the redirect arm, failing with what came back. */ const redirected = ( result: R, ): Extract => { if (result.status !== "redirect") { throw new Error(`oauth.start did not redirect: ${JSON.stringify(result)}`); } return result as Extract; }; scenario( "OAuth · probe discovers an authorization server's endpoints from its issuer URL", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const { client: makeClient } = yield* Api; const oauth = yield* serveOAuthTestServer(); const identity = yield* target.newIdentity(); const client = yield* makeClient(api, identity); const probed = yield* client.oauth.probe({ payload: { url: oauth.issuerUrl } }); expect(probed.authorizationUrl, "probe found the authorization endpoint").toBe( oauth.authorizationEndpoint, ); expect(probed.tokenUrl, "probe found the token endpoint").toBe(oauth.tokenEndpoint); }), ), ); scenario( "OAuth · a registered OAuth app is listed for its owner without leaking the secret", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const { client: makeClient } = yield* Api; const oauth = yield* serveOAuthTestServer(); const identity = yield* target.newIdentity(); const client = yield* makeClient(api, identity); const slug = OAuthClientSlug.make(unique("oauthc")); const created = yield* client.oauth.createClient({ payload: { owner: "org", slug, authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, grant: "authorization_code", clientId: "test-client", clientSecret: "test-secret", }, }); expect(created.client, "the app keeps the requested slug").toBe(slug); const clients = yield* client.oauth.listClients(); const mine = clients.find((entry) => entry.slug === slug); expect(mine, "the registered app appears in the owner's list").toMatchObject({ owner: "org", slug, grant: "authorization_code", clientId: "test-client", }); expect( JSON.stringify(clients), "the client secret never appears in the list projection", ).not.toContain("test-secret"); }), ), ); scenario( "OAuth · the authorization-code flow mints a connection (start → consent → complete)", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const { client: makeClient } = yield* Api; const oauth = yield* serveOAuthTestServer(); const identity = yield* target.newIdentity(); const client = yield* makeClient(api, identity); // An integration that declares an oauth auth template — the integration // is what the minted connection attaches to. const integration = IntegrationSlug.make(unique("oauthint")); yield* client.openapi.addSpec({ payload: { spec: { kind: "blob", value: JSON.stringify({ openapi: "3.0.3", info: { title: "OAuth-protected API", version: "1.0.0" }, paths: { "/me": { get: { operationId: "getMe", tags: ["default"], responses: { "200": { description: "the caller" } }, }, }, }, }), }, slug: integration, baseUrl: "http://127.0.0.1:59999", authenticationTemplate: [ { slug: "oauth", kind: "oauth2", authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, scopes: ["read"], }, ], }, }); const clientSlug = OAuthClientSlug.make(unique("oauthc")); yield* client.oauth.createClient({ payload: { owner: "org", slug: clientSlug, authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, grant: "authorization_code", clientId: "test-client", clientSecret: "test-secret", }, }); // start: the product persists a session and hands back the authorize URL. const started = redirected( yield* client.oauth.start({ payload: { client: clientSlug, clientOwner: "org", owner: "org", name: ConnectionName.make("main"), integration, template: AuthTemplateSlug.make("oauth"), }, }), ); expect(started.authorizationUrl, "the redirect points at the authorization server").toContain( oauth.authorizationEndpoint, ); // The user consents on the authorization server (headless here): the // authorize page bounces to the login form, and submitting credentials // redirects back to the product's callback with an authorization code. const authorize = yield* Effect.promise(() => fetch(started.authorizationUrl, { redirect: "manual" }), ); expect(authorize.status, "the authorize endpoint sends the user to log in").toBe(302); const consent = yield* Effect.promise(() => fetch(authorize.headers.get("location") ?? "", { method: "POST", redirect: "manual", headers: { authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`, }, }), ); expect(consent.status, "granting consent redirects back to the product").toBe(302); const callback = new URL(consent.headers.get("location") ?? ""); expect(callback.searchParams.get("state"), "the callback carries the session's state").toBe( String(started.state), ); const code = callback.searchParams.get("code"); expect(code, "the callback carries an authorization code").not.toBeNull(); // complete: the product exchanges the code and mints the connection. const connection = yield* client.oauth.complete({ payload: { state: started.state, code: code ?? "" }, }); expect(connection, "the minted connection is bound to the integration").toMatchObject({ owner: "org", name: "main", integration, template: "oauth", oauthClient: clientSlug, }); const connections = yield* client.connections.list({ query: { integration } }); expect( connections.map((c) => `${c.owner}/${String(c.name)}`), "the connection is listed for the integration", ).toContain("org/main"); }), ), ); scenario( "OAuth · cancelling an unknown session is idempotent", {}, Effect.gen(function* () { const target = yield* Target; const { client: makeClient } = yield* Api; const identity = yield* target.newIdentity(); const client = yield* makeClient(api, identity); const cancelled = yield* client.oauth.cancel({ payload: { state: OAuthState.make("oauth2_session_does_not_exist") }, }); expect(cancelled.cancelled, "cancel reports success even for an unknown session").toBe(true); }), );