feat(oauth): request explicit scopes instead of * on sign-in#2647
feat(oauth): request explicit scopes instead of * on sign-in#2647MattBro wants to merge 1 commit into
Conversation
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>
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix 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 |
| }).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", | ||
| ], | ||
| } | ||
| `); |
There was a problem hiding this 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)
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!
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_SCOPESnow mirrors the explicit set PostHog advertises as grantable — the API'sOAUTH_SCOPES_SUPPORTED, served asscopes_supportedat/.well-known/oauth-authorization-server— instead of["*"]. The embedded agent reaches most of the product through the PostHog MCPexectool, 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_VERSION5 → 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 onoauth.ts). A natural follow-up is to fetchscopes_supportedfrom the well-known endpoint at sign-in so it can't drift.How did you test this?
vitest run src/oauth.test.ts— theOAUTH_SCOPESguard snapshot passes with the new list + bumped version.biome checkandtsc --noEmitclean on@posthog/shared; the full-repo typecheck also ran in the pre-commit hook.Automatic notifications