History

Launch built-in Google OAuth for Calendar and Sheets

Rhys Sullivan committed 2 months ago

Commit ac3bea51 parent 67ee8a013 files · +313 −10Browse files

Showing the changes to packages/react/src/plugins (2 of 13 files). Show all

Unified · Split

2 files
  1. packages/react/src/plugins/use-effective-oauth-client.test.ts +23 −0
  2. packages/react/src/plugins/use-effective-oauth-client.tsx +29 −2
packages/react/src/plugins/use-effective-oauth-client.test.ts+23−0
@@ -293,6 +293,29 @@ describe("selectClientsForEndpoints", () => { · 292 unmodified lines
293 ]);293 ]);
294 });294 });
295 295
296 it("hides a scope-limited first-party app from another API on the same provider", () => {
297 const firstParty = app("first-party:google", {
298 owner: "org",
299 authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
300 tokenUrl: "https://oauth2.googleapis.com/token",
301 origin: {
302 kind: "first_party",
303 allowedScopes: ["openid", "email", "https://www.googleapis.com/auth/calendar"],
304 },
305 });
306 const result = selectClientsForEndpoints([firstParty], {
307 authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
308 tokenUrl: "https://oauth2.googleapis.com/token",
309 integration: IntegrationSlug.make("renamed_gmail"),
310 scopes: ["openid", "email", "https://mail.google.com/"],
311 });
312
313 expect(result.endpointMatched).toBe(false);
314 expect(result.matched).toEqual([]);
315 expect(result.nearMatches).toEqual([]);
316 expect(result.unmatched).toEqual([]);
317 });
318
296 it("intent-matches a first-party app to its declared integrations even without endpoints", () => {319 it("intent-matches a first-party app to its declared integrations even without endpoints", () => {
297 const integration = IntegrationSlug.make("github_rest");320 const integration = IntegrationSlug.make("github_rest");
298 const firstParty = app("first-party:github", {321 const firstParty = app("first-party:github", {
packages/react/src/plugins/use-effective-oauth-client.tsx+29−2
@@ -53,6 +53,19 @@ export const isDcrClient = (app: OAuthClientOption): boolean => · 52 unmodified lines
53export const isFirstPartyClient = (app: OAuthClientOption): boolean =>53export const isFirstPartyClient = (app: OAuthClientOption): boolean =>
54 app.origin.kind === "first_party";54 app.origin.kind === "first_party";
55 55
56/** Mirror the host's first-party scope boundary in the picker. The server is
57 * authoritative; this prevents offering a built-in app for a flow it will
58 * reject. An explicit policy with unknown scopes fails closed. */
59const firstPartyClientAllowsScopes = (
60 app: OAuthClientOption,
61 requestedScopes: readonly string[] | undefined,
62): boolean => {
63 if (app.origin.kind !== "first_party" || app.origin.allowedScopes === undefined) return true;
64 if (requestedScopes === undefined) return false;
65 const allowed = new Set(app.origin.allowedScopes);
66 return requestedScopes.every((scope) => allowed.has(scope));
67};
68
56const hostOf = (url: string): string | undefined => {69const hostOf = (url: string): string | undefined => {
57 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL() throws on invalid input; treat as "no host"70 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL() throws on invalid input; treat as "no host"
58 try {71 try {
@@ -162,6 +175,9 @@ export function selectClientsForEndpoints( · 103 unmodified lines
162 /** The integration whose picker this is. A manual app stamped with this175 /** The integration whose picker this is. A manual app stamped with this
163 * integration (recorded intent) is a tier-1 match regardless of host. */176 * integration (recorded intent) is a tier-1 match regardless of host. */
164 readonly integration?: IntegrationSlug;177 readonly integration?: IntegrationSlug;
178 /** Complete scope set declared by the selected OAuth auth method. Used to
179 * hide scope-limited first-party apps that the host will reject. */
180 readonly scopes?: readonly string[];
165 /** When set, an integration that targets a SPECIFIC server (MCP, whose181 /** When set, an integration that targets a SPECIFIC server (MCP, whose
166 * endpoints are discovered at connect) must match by endpoint — absent182 * endpoints are discovered at connect) must match by endpoint — absent
167 * endpoints mean NO match (show the register CTA), never "every app183 * endpoints mean NO match (show the register CTA), never "every app
@@ -175,7 +191,9 @@ export function selectClientsForEndpoints( · 7 unmodified lines
175 readonly endpointMatched: boolean;191 readonly endpointMatched: boolean;
176} {192} {
177 // DCR clients are plumbing, never picker options.193 // DCR clients are plumbing, never picker options.
178 const manual = all.filter((app) => !isDcrClient(app));194 const manual = all.filter(
195 (app) => !isDcrClient(app) && firstPartyClientAllowsScopes(app, endpoints.scopes),
196 );
179 197
180 const intent = endpoints.integration;198 const intent = endpoints.integration;
181 const matchesIntent = (app: OAuthClientOption): boolean => {199 const matchesIntent = (app: OAuthClientOption): boolean => {
@@ -262,6 +280,7 @@ export function useOAuthClientsForIntegration(opts: { · 80 unmodified lines
262 readonly tokenUrl?: string;280 readonly tokenUrl?: string;
263 readonly authorizationUrl?: string;281 readonly authorizationUrl?: string;
264 readonly integration?: IntegrationSlug;282 readonly integration?: IntegrationSlug;
283 readonly scopes?: readonly string[];
265 readonly requireEndpointMatch?: boolean;284 readonly requireEndpointMatch?: boolean;
266}): UseOAuthClientsResult {285}): UseOAuthClientsResult {
267 // Read the optimistic list so a just-registered/edited/removed app paints286 // Read the optimistic list so a just-registered/edited/removed app paints
@@ -283,9 +302,17 @@ export function useOAuthClientsForIntegration(opts: { · 15 unmodified lines
283 tokenUrl: opts.tokenUrl,302 tokenUrl: opts.tokenUrl,
284 authorizationUrl: opts.authorizationUrl,303 authorizationUrl: opts.authorizationUrl,
285 integration: opts.integration,304 integration: opts.integration,
305 scopes: opts.scopes,
286 requireEndpointMatch: opts.requireEndpointMatch,306 requireEndpointMatch: opts.requireEndpointMatch,
287 }),307 }),
288 [all, opts.tokenUrl, opts.authorizationUrl, opts.integration, opts.requireEndpointMatch],308 [
309 all,
310 opts.tokenUrl,
311 opts.authorizationUrl,
312 opts.integration,
313 opts.scopes,
314 opts.requireEndpointMatch,
315 ],
289 );316 );
290 317
291 if (!loaded) {318 if (!loaded) {

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