Skip to content

fix: store granted scopes in credentials, warn on scope escalation via export#202

Open
efe-arv wants to merge 1 commit intogoogleworkspace:mainfrom
efe-arv:fix/scope-bound-credentials
Open

fix: store granted scopes in credentials, warn on scope escalation via export#202
efe-arv wants to merge 1 commit intogoogleworkspace:mainfrom
efe-arv:fix/scope-bound-credentials

Conversation

@efe-arv
Copy link

@efe-arv efe-arv commented Mar 5, 2026

Closes #168

Problem

When users authenticate with --readonly and export credentials via gws 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

File Change
src/auth_commands.rs handle_login now persists granted_scopes array in the encrypted credential JSON
src/auth_commands.rs handle_export warns when credentials were obtained with readonly scopes (or lack scope metadata entirely)
src/auth.rs load_credentials_inner reads granted_scopes and warns when a requested scope was not in the original grant
src/auth_commands.rs 2 new tests: credentials_json_includes_granted_scopes, credentials_without_granted_scopes_is_legacy

How 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):

⚠️  WARNING: These credentials were obtained with read-only scopes,
but the exported refresh token is NOT scope-limited by Google.
Anyone with this token can request write access.
Consider using a separate OAuth client for read-only use cases.

Runtime (scope enforcement):

⚠️  Scope 'https://...drive' was not in the granted scopes at login time.
Re-run 'gws auth login' with the required scopes.

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

  • Credentials without granted_scopes (created before this fix) continue to work
  • A separate warning is shown when exporting legacy credentials without scope metadata
  • No breaking changes to the credential format (granted_scopes is an optional field)

…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.
@efe-arv efe-arv requested a review from jpoehnelt as a code owner March 5, 2026 20:33
@gemini-code-assist
Copy link
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

⚠️ No Changeset found

Latest commit: ddd12da

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@google-cla
Copy link

google-cla bot commented Mar 5, 2026

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.

@jpoehnelt jpoehnelt added area: auth cla: no This human has *not* signed the Contributor License Agreement. complexity: medium Moderate change, some review needed labels Mar 5, 2026
@jpoehnelt
Copy link
Member

Missing CLA

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

Labels

area: auth cla: no This human has *not* signed the Contributor License Agreement. complexity: medium Moderate change, some review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gws auth login --readonly + auth export --unmasked appears to allow full access on external machine

3 participants