Skip to content

feat(oauth): request explicit scopes instead of * on sign-in#2647

Draft
MattBro wants to merge 1 commit into
mainfrom
matt/cli-scopes-off-star
Draft

feat(oauth): request explicit scopes instead of * on sign-in#2647
MattBro wants to merge 1 commit into
mainfrom
matt/cli-scopes-off-star

Conversation

@MattBro

@MattBro MattBro commented Jun 12, 2026

Copy link
Copy Markdown

Problem

The desktop app requests the * wildcard scope at sign-in, so its OAuth token can do anything the user can. That's the single biggest source of *-scoped tokens in production, and it's what blocks retiring the * wildcard server-side — the app keeps minting wildcard tokens.

Changes

  • OAUTH_SCOPES now mirrors the explicit set PostHog advertises as grantable — the API's OAUTH_SCOPES_SUPPORTED, served as scopes_supported at /.well-known/oauth-authorization-server — instead of ["*"]. The embedded agent reaches most of the product through the PostHog MCP exec tool, so it genuinely needs the broad unprivileged set; this drops only the privileged/internal scopes and the literal *, and stops newly-added scopes from being auto-granted.
  • OAUTH_SCOPE_VERSION 5 → 6, which forces existing installs to re-authorize with the narrower set on next use.

Only touches the desktop/shared client (the one on *). The mobile app is a separate OAuth client and already requests a curated 8-scope list — left unchanged.

The list is kept in sync with the API's generated oauth-scopes.generated.ts (noted in a comment on oauth.ts). A natural follow-up is to fetch scopes_supported from the well-known endpoint at sign-in so it can't drift.

How did you test this?

  • vitest run src/oauth.test.ts — the OAUTH_SCOPES guard snapshot passes with the new list + bumped version.
  • biome check and tsc --noEmit clean on @posthog/shared; the full-repo typecheck also ran in the pre-commit hook.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Mirror the scopes PostHog advertises as grantable (the API's OAUTH_SCOPES_SUPPORTED,
served at /.well-known/oauth-authorization-server) instead of requesting the "*"
wildcard, and bump OAUTH_SCOPE_VERSION 5->6 to force existing users to re-authorize
with the narrower set. Keeps the desktop token least-privilege: no privileged or
internal scopes, and newly-added scopes are not auto-granted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 26d086d.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/shared/src/oauth.test.ts:9-194
**Large inline snapshot should move to an external file**

The inline snapshot grew from 3 lines (just `"*"`) to 185 lines. Per the team's style rule, extensive snapshots belong in external `.snap` files — keeping them inline makes diffs noisy and the test file hard to navigate. Vitest supports `toMatchSnapshot()` with an auto-managed `__snapshots__/oauth.test.ts.snap` file that would serve the same guard purpose here.

Reviews (1): Last reviewed commit: "feat(oauth): request explicit scopes ins..." | Re-trigger Greptile

Comment on lines 9 to 194
}).toMatchInlineSnapshot(`
{
"scopeVersion": 5,
"scopeVersion": 6,
"scopes": [
"*",
"openid",
"profile",
"email",
"action:read",
"action:write",
"access_control:read",
"access_control:write",
"account:read",
"account:write",
"activity_log:read",
"activity_log:write",
"alert:read",
"alert:write",
"annotation:read",
"annotation:write",
"approvals:read",
"approvals:write",
"batch_export:read",
"batch_export:write",
"batch_import:read",
"batch_import:write",
"business_knowledge:read",
"business_knowledge:write",
"cohort:read",
"cohort:write",
"comment:read",
"comment:write",
"conversation:read",
"conversation:write",
"customer_analytics:read",
"customer_analytics:write",
"customer_journey:read",
"customer_journey:write",
"customer_profile_config:read",
"customer_profile_config:write",
"dashboard:read",
"dashboard:write",
"event_filter:read",
"event_filter:write",
"dashboard_template:read",
"dashboard_template:write",
"dataset:read",
"dataset:write",
"desktop_recording:read",
"desktop_recording:write",
"early_access_feature:read",
"early_access_feature:write",
"endpoint:read",
"endpoint:write",
"engineering_analytics:read",
"engineering_analytics:write",
"error_tracking:read",
"error_tracking:write",
"evaluation:read",
"evaluation:write",
"element:read",
"element:write",
"event_definition:read",
"event_definition:write",
"experiment:read",
"experiment:write",
"experiment_saved_metric:read",
"experiment_saved_metric:write",
"export:read",
"export:write",
"external_data_schema:read",
"external_data_schema:write",
"external_data_source:read",
"external_data_source:write",
"feature_flag:read",
"feature_flag:write",
"file_system:read",
"file_system:write",
"file_system_shortcut:read",
"file_system_shortcut:write",
"group:read",
"group:write",
"health_issue:read",
"health_issue:write",
"heatmap:read",
"heatmap:write",
"hog_flow:read",
"hog_flow:write",
"hog_function:read",
"hog_function:write",
"insight:read",
"insight:write",
"insight_variable:read",
"insight_variable:write",
"integration:read",
"integration:write",
"legal_document:read",
"legal_document:write",
"link:read",
"link:write",
"live_debugger:read",
"live_debugger:write",
"llm_analytics:read",
"llm_analytics:write",
"llm_prompt:read",
"llm_prompt:write",
"llm_provider_key:read",
"llm_provider_key:write",
"llm_skill:read",
"llm_skill:write",
"logs:read",
"logs:write",
"marketing_analytics:read",
"marketing_analytics:write",
"notebook:read",
"notebook:write",
"organization:read",
"organization:write",
"organization_integration:read",
"organization_integration:write",
"organization_member:read",
"organization_member:write",
"person:read",
"person:write",
"persisted_folder:read",
"persisted_folder:write",
"plugin:read",
"plugin:write",
"product_tour:read",
"product_tour:write",
"project:read",
"project:write",
"property_definition:read",
"property_definition:write",
"query:read",
"query:write",
"replay_scanner:read",
"replay_scanner:write",
"revenue_analytics:read",
"revenue_analytics:write",
"session_recording:read",
"session_recording:write",
"session_recording_playlist:read",
"session_recording_playlist:write",
"sharing_configuration:read",
"sharing_configuration:write",
"signal_scout:read",
"signal_scout:write",
"streamlit_app:read",
"streamlit_app:write",
"subscription:read",
"subscription:write",
"survey:read",
"survey:write",
"tagger:read",
"tagger:write",
"ticket:read",
"ticket:write",
"task:read",
"task:write",
"tracing:read",
"tracing:write",
"field_note:read",
"field_note:write",
"uploaded_media:read",
"uploaded_media:write",
"usage_metric:read",
"usage_metric:write",
"user:read",
"user:write",
"user_interview:read",
"user_interview:write",
"visual_review:read",
"visual_review:write",
"warehouse_objects:read",
"warehouse_objects:write",
"warehouse_table:read",
"warehouse_table:write",
"warehouse_view:read",
"warehouse_view:write",
"web_analytics:read",
"web_analytics:write",
"webhook:read",
"webhook:write",
],
}
`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Large inline snapshot should move to an external file

The inline snapshot grew from 3 lines (just "*") to 185 lines. Per the team's style rule, extensive snapshots belong in external .snap files — keeping them inline makes diffs noisy and the test file hard to navigate. Vitest supports toMatchSnapshot() with an auto-managed __snapshots__/oauth.test.ts.snap file that would serve the same guard purpose here.

Rule Used: Avoid using inline snapshots for large test result... (source)

Learned From
PostHog/posthog#32651

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/shared/src/oauth.test.ts
Line: 9-194

Comment:
**Large inline snapshot should move to an external file**

The inline snapshot grew from 3 lines (just `"*"`) to 185 lines. Per the team's style rule, extensive snapshots belong in external `.snap` files — keeping them inline makes diffs noisy and the test file hard to navigate. Vitest supports `toMatchSnapshot()` with an auto-managed `__snapshots__/oauth.test.ts.snap` file that would serve the same guard purpose here.

**Rule Used:** Avoid using inline snapshots for large test result... ([source](https://app.greptile.com/posthog-org-19734/-/custom-context?memory=22c98109-c304-4258-9763-34ef5bd39e6d))

**Learned From**
[PostHog/posthog#32651](https://github.com/PostHog/posthog/pull/32651)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant