fix: store granted scopes in credentials, warn on scope escalation via export#202
fix: store granted scopes in credentials, warn on scope escalation via export#202efe-arv wants to merge 1 commit intogoogleworkspace:mainfrom
Conversation
…a export Closes googleworkspace#168 ## Problem When users authenticate with `--readonly` and then export credentials via `gws auth export --unmasked`, the exported refresh token can be used to request write access on another machine. This is because OAuth2 refresh tokens are not scope-limited by Google — the token can mint access tokens for any scope the OAuth app is authorized for. ## Changes ### 1. Store granted scopes in credential file (`auth_commands.rs`) `handle_login` now persists `granted_scopes` alongside the refresh token in the encrypted credential JSON: ```json { "type": "authorized_user", "client_id": "...", "client_secret": "...", "refresh_token": "...", "granted_scopes": ["https://...drive.readonly", ...] } ``` ### 2. Warn on export (`auth_commands.rs`) `handle_export` now checks for scope metadata and prints warnings: - If credentials have only readonly scopes: warns that the refresh token is NOT scope-limited and can be used for write access - If credentials lack scope metadata (pre-fix): warns about potential scope escalation ### 3. Enforce scope boundaries on token requests (`auth.rs`) `load_credentials_inner` now reads `granted_scopes` from encrypted credentials and warns when a requested scope was not in the original grant. This catches scope escalation attempts at runtime. ### 4. Tests - `credentials_json_includes_granted_scopes`: verifies scope persistence - `credentials_without_granted_scopes_is_legacy`: verifies backward compat ## Note on OAuth2 limitation This fix mitigates but cannot fully prevent scope escalation, because Google OAuth2 refresh tokens are fundamentally not scope-bound. The refresh token can always request any scope the OAuth app was configured for. True scope isolation requires separate OAuth clients per access level. The warning in `handle_export` documents this limitation.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
Missing CLA |
Closes #168
Problem
When users authenticate with
--readonlyand export credentials viagws auth export --unmasked, the exported refresh token can be used to request write access on another machine. This happens because OAuth2 refresh tokens are not scope-limited by Google — the token can mint access tokens for any scope the OAuth app is authorized for.Changes
src/auth_commands.rshandle_loginnow persistsgranted_scopesarray in the encrypted credential JSONsrc/auth_commands.rshandle_exportwarns when credentials were obtained with readonly scopes (or lack scope metadata entirely)src/auth.rsload_credentials_innerreadsgranted_scopesand warns when a requested scope was not in the original grantsrc/auth_commands.rscredentials_json_includes_granted_scopes,credentials_without_granted_scopes_is_legacyHow it works
Login (credential creation):
{ "type": "authorized_user", "client_id": "...", "client_secret": "...", "refresh_token": "...", "granted_scopes": ["https://...drive.readonly", "https://...gmail.readonly"] }Export (credential sharing):
Runtime (scope enforcement):
OAuth2 limitation
This fix mitigates but cannot fully prevent scope escalation. Google OAuth2 refresh tokens are fundamentally not scope-bound — the token can always request any scope the app was configured for. True scope isolation requires separate OAuth clients per access level. The export warning documents this limitation explicitly.
Backward compatibility
granted_scopes(created before this fix) continue to workgranted_scopesis an optional field)