Repository navigation
feat(auth): harden auth add --readonly for least-privilege consumers - #54
Merged
Merged
Conversation
Three gaps let a read-only authorization end up write-capable or pass through the relay: - include_granted_scopes: the consent URL always asked Google to fold earlier grants into the new token, so `--readonly` after a full login could return a refresh token that still carried full `drive`. A read-only authorization (browser, manual, remote and headless) now leaves the parameter out. `auth manage` keeps it. - Merge: a read-only `auth add` (and `auth poll --readonly`) merged into the token already stored for the same client+email. Over a write-scoped token that replaced a working write login with a read-only one while the recorded scopes still claimed write access. It now refuses before the OAuth flow starts, and again at store time (secrets.MergeReadOnlyToken), exits 2 and says to use a dedicated --client (e.g. --client brain-ro). Merging into a read-only token is unchanged. - Relay: `auth add --no-relay` never uses the headless callback relay. Config auth_mode=headless and no-TTY auto-detection resolve to the local loopback flow, and --headless, --no-poll and --callback-server are rejected. --manual and --remote still work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…giene Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x client docs A stored token that records no scopes (for example one brought in by `auth tokens import`) is not provably read-only. A read-only `auth add` or `auth poll --readonly` now refuses it with exit 2 and the same dedicated --client hint, instead of overwriting a token that may hold write access. docs/auth.md wrongly said a named client without its own credentials file uses default's OAuth client ID. ReadClientCredentialsFor never reads default's credentials.json: it tries the client's own file, then the built-in client ID, then WK_CLIENT_ID/WK_CLIENT_SECRET, and fails otherwise. The docs now say that, and that two clients resolving to the built-in client ID are one OAuth client to Google. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
Read-only auth hygiene for
wk auth add ... --readonly:include_granted_scopeson a read-only authorization.authURLParams(forceConsent, readonly)leaves the parameter out whenAuthorizeOptions.Readonly/HeadlessOptions.Readonlyis set. This applies to the browser, manual, remote and headless paths. Without it, Google could fold an earlier fulldrivegrant into the new "read-only" refresh token.auth manageis unchanged.secrets.CheckReadOnlyMerge/secrets.MergeReadOnlyToken. A read-onlyauth add(andauth poll --readonly) refuses when the token stored for the same client+email records write scopes. The check runs before the OAuth flow starts (browser/manual, remote step 1, headless) and again at store time. The command exits 2 with:... use a dedicated --client (e.g. --client brain-ro) for read-only access. The existing token is left untouched. Merging into a read-only token works as before. A keyring read error propagates and is never treated as "no token".--no-relayonauth add.ResolveAuthMode(ctx, headless, manual, noRelay, callbackServer): configauth_mode: headlessand no-TTY auto-detection resolve to the loopback browser flow, and the relay's/healthis never contacted.--headless,--no-polland--callback-servertogether with--no-relayare usage errors.--manual/--remotestill work.Also:
docs/auth.md..deadcode-baseline.txtis updated for the line shift ofwaitPostSuccessinoauth_flow.go(308 → 316). No new dead code.Tests
oauth_flow_more_test.go:TestAuthURLParams_ReadonlyOmitsIncludeGrantedScopesandTestHeadlessAuthorize_ReadonlyOmitsIncludeGrantedScopes. The existingTestAuthURLParamsstill assertsinclude_granted_scopes=truefor the full flow.detect_test.go:--no-relayoverrides config headless; it skips auto-detect headless without calling/health; manual is kept.internal/secrets/readonly_merge_test.go: first authorization, merge into a read-only token, refusal over a write-scoped token (token untouched), a dedicated client being independent, and read-error propagation.internal/cmd/auth_readonly_hygiene_test.go: Readonly is plumbed to the consent URL; refusal before OAuth (exit 2, fix text);--client brain-roaccepted alongside a write-scopeddefault; refusal in remote step 1, headless andauth poll --readonly;--no-relayforces loopback over config headless (with a relay control run);--no-relayrejects relay flags;--no-relay --manual.Gate:
make ciexits 0 (fmt-check, golangci-lint--new-from-rev=origin/main, test, deadcode, race, coverage).Note: a named client without its own
credentials-<client>.jsonfalls back to the same embedded OAuth client ID asdefault.--client brain-rothen separates the tokens in the local keyring, but Google still sees one OAuth client. The docs say to register a second OAuth client for Google-side separation.internal/googleauth/readonly_scopes.go(+ test) is byte-identical to the copy in the companionwk auth scopesPR.Consumer: khal/brain GSuite connector (read-only). brain authorizes with
wk --client brain-ro auth add <email> --readonly --no-relay.🤖 Generated with Claude Code