Skip to content

fix(auth): enforce readonly scopes by revoking stale tokens and adding client-side guard#520

Open
gerfalcon wants to merge 5 commits intogoogleworkspace:mainfrom
gerfalcon:fix/issue-168-readonly-scope-enforcement
Open

fix(auth): enforce readonly scopes by revoking stale tokens and adding client-side guard#520
gerfalcon wants to merge 5 commits intogoogleworkspace:mainfrom
gerfalcon:fix/issue-168-readonly-scope-enforcement

Conversation

@gerfalcon
Copy link

@gerfalcon gerfalcon commented Mar 17, 2026

Summary

Fixes #168.

gws auth login --readonly doesn't actually enforce read-only access when the user previously logged in with broader scopes. The refresh token keeps its original grants, and Google ignores the scope param on refresh — so the token silently has write access.

What this PR does

  1. Saves configured scopes to scopes.json on login so we can detect scope changes later
  2. Revokes the old refresh token when scopes change, clearing local creds before re-authenticating — this removes the prior consent grant so Google only shows the new scopes
  3. Blocks write operations client-side when in a readonly session (defense-in-depth)
  4. Shows scope_mode in gws auth status for transparency

Why just prompt=consent isn't enough

Google's consent screen shows previously-granted scopes pre-checked. Users click "Allow" and unknowingly re-grant broad access. Revoking the token first removes the prior grant entirely.

Test plan

  • 6 new unit tests, full suite passes (636 tests)
  • cargo clippy -- -D warnings clean
  • Manual: gws auth logingws auth login --readonly → write op blocked → gws auth login → write op works

…g client-side guard

When a user previously authenticated with full scopes, `gws auth login
--readonly` did not actually enforce read-only access. The underlying
refresh token retained the original consent grant, and Google's token
endpoint ignores the scope parameter on refresh requests — so the cached
token silently carried write access.

This commit fixes the issue with a layered approach:

- Persist the configured scope set to scopes.json on login so scope
  changes can be detected across sessions.
- When scopes change, revoke the old refresh token via Google's
  revocation endpoint and clear local credential/cache files before
  starting the new OAuth flow.
- Add a client-side scope guard that rejects write-scope API requests
  when the session is readonly.
- Show scope_mode and configured_scopes in `gws auth status`.
- Clean up scopes.json on logout.

Fixes googleworkspace#168
@gerfalcon gerfalcon requested a review from jpoehnelt as a code owner March 17, 2026 16:42
@changeset-bot
Copy link

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: 14a2dfe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Patch

Not sure what this means? Click here to learn what changesets are.

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

@googleworkspace-bot googleworkspace-bot added area: auth area: core Core CLI parsing, commands, error handling, utilities labels Mar 17, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical security vulnerability where gws auth login --readonly failed to enforce read-only access if a user had previously authenticated with broader permissions. The changes introduce a robust, multi-layered approach to ensure that requested scopes are strictly adhered to, preventing unintended write access and providing clearer feedback to the user about their current authentication state.

Highlights

  • Scope persistence: Configured scopes are now saved to scopes.json to detect changes across sessions.
  • Token revocation on scope change: Old refresh tokens are revoked via Google's endpoint and local credentials cleared if requested scopes differ from previously saved ones, ensuring new consent grants are clean.
  • Client-side scope guard: Before API calls, the system verifies scope compatibility, rejecting write-scope requests in read-only sessions with an informative error.
  • Status display: gws auth status now shows scope_mode and configured_scopes for improved transparency.
Changelog
  • .changeset/fix-readonly-scope-enforcement.md
    • Added a new changeset entry for the readonly scope enforcement fix.
  • src/auth_commands.rs
    • Introduced scopes_path, save_scopes, load_saved_scopes, is_readonly_session, and check_scope_allowed functions to manage and enforce authentication scopes.
    • Implemented logic within handle_login to detect scope changes, revoke previous refresh tokens, and clear local credentials.
    • Enhanced handle_status to output scope_mode and configured_scopes for better transparency.
    • Modified handle_logout to ensure the scopes.json file is removed upon logout.
    • Added comprehensive unit tests for scope persistence, readonly session detection, and client-side scope guarding.
  • src/main.rs
    • Integrated auth_commands::check_scope_allowed to prevent write operations in read-only sessions.
Activity
  • Unit tests for scope persistence, readonly detection, and scope guard (6 new tests) have been implemented and passed.
  • cargo clippy -- -D warnings passed without warnings.
  • The full test suite (445 tests) passed.
  • Manual test cases for gws auth login (full to readonly), gws gmail +send in readonly, gws auth status display, gws auth login (full) for write operations, and gws auth logout cleanup are outlined in the PR description.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a robust mechanism for enforcing read-only scopes by persisting scope information, revoking stale tokens on scope changes, and adding a client-side guard. The implementation is well-structured and addresses the reported issue effectively.

I have one main suggestion for improvement:
Add error handling to the save_scopes function to prevent silent failures and ensure application state consistency.

This change will make the new authentication flow more robust.

Keep both upstream's new tests (find_unmatched_services,
extract_scopes_from_doc) and our scope persistence/guard tests.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a robust mechanism for enforcing read-only scopes, which is a critical security improvement. The layered approach of persisting scopes, revoking tokens on scope change, and adding a client-side guard is well-thought-out. My review includes a suggestion to improve error handling when saving scopes to ensure the system's state is always consistent. Overall, this is a solid contribution to improving the tool's security and reliability.

Address review feedback: save_scopes now returns Result<(), GwsError>
and the call site uses ? to propagate failures. This ensures scopes.json
is always consistent with the actual login state.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses the issue of enforcing read-only scopes by revoking stale tokens upon scope change and adding a client-side guard. The implementation is robust and includes new unit tests. I've identified a couple of areas for improvement regarding code duplication and test structure that would enhance the maintainability and reliability of the new logic. My comments focus on refactoring to remove duplicated code and making the tests less brittle.

…ation

DRY up the scope classification logic that was duplicated between
is_readonly_session and check_scope_allowed. Rewrite tests to call the
extracted helper directly instead of re-implementing the logic.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to enforce read-only scopes by revoking old tokens and adding a client-side guard. The implementation is a solid step towards fixing the security issue. I've identified a critical issue where the failure to delete old credential files is ignored, which could lead to an inconsistent state and bypass the intended scope enforcement. I've also pointed out a high-severity issue where the failure of the token revocation API call is silently ignored. Addressing these points will make the implementation more robust and secure.

Warn the user when token revocation fails (network error or non-200
response) so they know the old token may still be valid server-side.

Return errors when credential/cache file removal fails (ignoring
NotFound) instead of silently continuing with stale files.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses a security flaw in read-only scope enforcement by revoking stale tokens when scopes change. The implementation is robust, including a client-side guard as a defense-in-depth measure. My review includes one high-severity security recommendation to prevent terminal escape sequence injection by sanitizing error output, in line with the repository's general rules.

Comment on lines +354 to +357
eprintln!(
"Warning: could not revoke old token ({e}). \
The old token may still be valid on Google's side."
);
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

Printing the reqwest::Error directly to the terminal using the default Display trait can be a security risk. If the error is related to DNS resolution or other network issues where an attacker can control parts of the error message, it could lead to terminal escape sequence injection. This could be used to trick the user or hide other malicious activity on the terminal.

To mitigate this, you should use the Debug format specifier ({:?}), which is designed for developer output and will safely escape control characters.

                                eprintln!(
                                    "Warning: could not revoke old token ({:?}). \
                                     The old token may still be valid on Google's side.",
                                    e
                                );
References
  1. Sanitize error strings printed to the terminal to prevent escape sequence injection.

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

Labels

area: auth area: core Core CLI parsing, commands, error handling, utilities

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

2 participants