Skip to content

Commit 739805f

Browse files
authored
Support GitHub App OAuth permissions (#1659)
1 parent 34f3720 commit 739805f

4 files changed

Lines changed: 49 additions & 4 deletions

File tree

‎apps/cloud/src/engine/execution-stack.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ const cloudFirstPartyOAuthClients = (): readonly FirstPartyOAuthClientConfig[] =
126126
env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token",
127127
clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID,
128128
clientSecret: env.FIRST_PARTY_GITHUB_CLIENT_SECRET,
129+
integrations: [IntegrationSlug.make("github_rest")],
130+
// GitHub App user access tokens do not use classic OAuth scopes;
131+
// their capabilities come from the app's registered permissions.
132+
authorizationScopes: [],
129133
},
130134
]
131135
: []),

‎packages/core/sdk/src/oauth-client.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ export interface FirstPartyOAuthClientConfig {
129129
* exact-match default for those integrations. Endpoint-host matching still
130130
* applies when omitted. */
131131
readonly integrations?: readonly IntegrationSlug[];
132+
/** Scopes sent on the provider authorization request instead of the
133+
* integration-declared set. Use an empty array for providers such as
134+
* GitHub Apps, whose capabilities are configured on the app and whose OAuth
135+
* user-token flow does not use scopes. Omit for normal OAuth clients. */
136+
readonly authorizationScopes?: readonly string[];
132137
/** OAuth scopes this deployment permits the app to request. Omit to allow
133138
* every scope declared by a matching integration. For declared scopes,
134139
* start and completion fail unless every requested scope belongs to this

‎packages/core/sdk/src/oauth-first-party.test.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from "./oauth-client";
1818
import { definePlugin } from "./plugin";
1919
import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config";
20-
import { serveOAuthTestServer } from "./testing/oauth-test-server";
20+
import { scopesFromAuthorizeUrl, serveOAuthTestServer } from "./testing/oauth-test-server";
2121

2222
// First-party OAuth clients: host-operated apps declared in executor config
2323
// (`firstPartyOAuthClients`), addressed as `first-party:<name>`. Resolved from
@@ -165,6 +165,40 @@ describe("first-party oauth clients", () => {
165165
},
166166
);
167167

168+
it.effect("an authorization scope override supports scope-less provider app tokens", () =>
169+
Effect.scoped(
170+
Effect.gen(function* () {
171+
const server = yield* serveOAuthTestServer({ scopes: ["repo"] });
172+
const { executor } = yield* makeTestWorkspaceHarness({
173+
plugins,
174+
firstPartyOAuthClients: [{ ...firstPartyClientFor(server), authorizationScopes: [] }],
175+
});
176+
yield* executor.acme.seed(["repo"]);
177+
178+
const started = yield* executor.oauth.start({
179+
owner: "org",
180+
client: FIRST_PARTY,
181+
clientOwner: "org",
182+
name: ConnectionName.make("github-app"),
183+
integration: INTEG,
184+
template: TEMPLATE,
185+
});
186+
expect(started.status).toBe("redirect");
187+
if (started.status !== "redirect") return;
188+
expect(scopesFromAuthorizeUrl(started.authorizationUrl)).toEqual([]);
189+
190+
const callback = yield* server.completeAuthorizationCodeFlow({
191+
authorizationUrl: started.authorizationUrl,
192+
});
193+
const connection = yield* executor.oauth.complete({
194+
state: started.state,
195+
code: callback.code,
196+
});
197+
expect(connection.oauthScope).toBeNull();
198+
}),
199+
),
200+
);
201+
168202
it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () =>
169203
Effect.scoped(
170204
Effect.gen(function* () {

‎packages/core/sdk/src/oauth-service.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,9 +1302,11 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
13021302
// list is already authoritative (§7.2) and must not be re-narrowed by a
13031303
// divergent authorization server.
13041304
const authorizationRequestedScopes =
1305-
scopePolicy.kind === "discover"
1306-
? requestedScopes
1307-
: yield* filterAuthorizationCodeScopes(client, requestedScopes);
1305+
firstParty?.authorizationScopes !== undefined
1306+
? dedupeScopes(firstParty.authorizationScopes)
1307+
: scopePolicy.kind === "discover"
1308+
? requestedScopes
1309+
: yield* filterAuthorizationCodeScopes(client, requestedScopes);
13081310

13091311
// authorization_code: persist a session + build the authorize URL.
13101312
const verifier = createPkceCodeVerifier();

0 commit comments

Comments
 (0)