Unified · Split
| @@ -126,6 +126,10 @@ const cloudFirstPartyOAuthClients = (): readonly FirstPartyOAuthClientConfig[] = · 125 unmodified lines | |||
| 126 | env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token", | 126 | env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token", |
| 127 | clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID, | 127 | clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID, |
| 128 | clientSecret: env.FIRST_PARTY_GITHUB_CLIENT_SECRET, | 128 | 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: [], | ||
| 129 | }, | 133 | }, |
| 130 | ] | 134 | ] |
| 131 | : []), | 135 | : []), |
| @@ -129,6 +129,11 @@ export interface FirstPartyOAuthClientConfig { · 128 unmodified lines | |||
| 129 | * exact-match default for those integrations. Endpoint-host matching still | 129 | * exact-match default for those integrations. Endpoint-host matching still |
| 130 | * applies when omitted. */ | 130 | * applies when omitted. */ |
| 131 | readonly integrations?: readonly IntegrationSlug[]; | 131 | 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[]; | ||
| 132 | /** OAuth scopes this deployment permits the app to request. Omit to allow | 137 | /** OAuth scopes this deployment permits the app to request. Omit to allow |
| 133 | * every scope declared by a matching integration. For declared scopes, | 138 | * every scope declared by a matching integration. For declared scopes, |
| 134 | * start and completion fail unless every requested scope belongs to this | 139 | * start and completion fail unless every requested scope belongs to this |
| @@ -17,7 +17,7 @@ import { · 16 unmodified lines | |||
| 17 | } from "./oauth-client"; | 17 | } from "./oauth-client"; |
| 18 | import { definePlugin } from "./plugin"; | 18 | import { definePlugin } from "./plugin"; |
| 19 | import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config"; | 19 | import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config"; |
| 20 | import { serveOAuthTestServer } from "./testing/oauth-test-server"; | 20 | import { scopesFromAuthorizeUrl, serveOAuthTestServer } from "./testing/oauth-test-server"; |
| 21 | 21 | ||
| 22 | // First-party OAuth clients: host-operated apps declared in executor config | 22 | // First-party OAuth clients: host-operated apps declared in executor config |
| 23 | // (`firstPartyOAuthClients`), addressed as `first-party:<name>`. Resolved from | 23 | // (`firstPartyOAuthClients`), addressed as `first-party:<name>`. Resolved from |
| @@ -165,6 +165,40 @@ describe("first-party oauth clients", () => { · 141 unmodified lines | |||
| 165 | }, | 165 | }, |
| 166 | ); | 166 | ); |
| 167 | 167 | ||
| 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 | |||
| 168 | it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () => | 202 | it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () => |
| 169 | Effect.scoped( | 203 | Effect.scoped( |
| 170 | Effect.gen(function* () { | 204 | Effect.gen(function* () { |
| @@ -1302,9 +1302,11 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { · 1301 unmodified lines | |||
| 1302 | // list is already authoritative (§7.2) and must not be re-narrowed by a | 1302 | // list is already authoritative (§7.2) and must not be re-narrowed by a |
| 1303 | // divergent authorization server. | 1303 | // divergent authorization server. |
| 1304 | const authorizationRequestedScopes = | 1304 | const authorizationRequestedScopes = |
| 1305 | scopePolicy.kind === "discover" | 1305 | firstParty?.authorizationScopes !== undefined |
| 1306 | ? requestedScopes | 1306 | ? dedupeScopes(firstParty.authorizationScopes) |
| 1307 | : yield* filterAuthorizationCodeScopes(client, requestedScopes); | 1307 | : scopePolicy.kind === "discover" |
| 1308 | ? requestedScopes | ||
| 1309 | : yield* filterAuthorizationCodeScopes(client, requestedScopes); | ||
| 1308 | 1310 | ||
| 1309 | // authorization_code: persist a session + build the authorize URL. | 1311 | // authorization_code: persist a session + build the authorize URL. |
| 1310 | const verifier = createPkceCodeVerifier(); | 1312 | const verifier = createPkceCodeVerifier(); |