Repository navigation
Description
Summary
After re-authenticating (gws auth login -s drive,docs), gws continued returning 403 insufficient authentication scopes on every Drive API call, while Gmail-scoped calls succeeded fine throughout. The login command's own output correctly listed https://www.googleapis.com/auth/drive as a granted scope, and ~/.config/gws/credentials.enc had a fresh timestamp — but the actual Drive API calls kept failing as if that scope had never been granted.
Environment
gwsversion: 0.11.1 (@googleworkspace/cli)- macOS 15.7.3 (Build 24G419)
- Auth method: OAuth2, installed/desktop client (
redirect_uris: ["http://localhost"]) - Keyring backend:
keyring
What was ruled out before finding the real cause
Extensive troubleshooting confirmed this was not caused by any of the following (each checked directly):
- OAuth consent screen scope configuration (Drive API confirmed
ENABLEDviagcloud services list) - OAuth consent screen test users list (account confirmed present)
- A stray local process holding a port from a previous
gws auth logininvocation (found and killed one, no change) - A full OS reboot (no change — rules out any in-memory/process-level caching)
client_secret.jsonvalidity (if this were invalid, login itself would fail withinvalid_client, not succeed and then fail on API calls — and other-scoped calls, e.g. Gmail, worked fine throughout)
Multiple full gws auth login -s drive,docs re-runs across this window, including after switching to a different Google account and back, did not resolve the issue.
Root cause (as best diagnosed)
~/.config/gws/ contains two separate local token/credential stores:
credentials.enc(encrypted, gets a fresh timestamp on everygws auth login)token_cache.json(binary — not actually JSON despite the extension/name; failed UTF-8 decode when inspected)
A normal gws auth login re-auth appears to refresh credentials.enc but not fully invalidate/replace whatever state token_cache.json holds (or vice versa — the exact read/write interaction between the two wasn't fully traced from the outside). The practical symptom: the CLI looks like it completed a fresh login with the requested scope, but the token actually used for API calls doesn't carry that scope.
Workaround that fixed it
Manually deleting the cached credential/token state (while preserving the app config) and then doing a completely fresh login resolved the issue immediately:
rm ~/.config/gws/credentials.enc ~/.config/gws/token_cache.json
rm -rf ~/.config/gws/cache/* # cached API discovery docs, unrelated to auth but safe to clear too
gws auth login -s drive,docs(client_secret.json and .encryption_key were left untouched — those are the app identity/encryption key, not per-session state.)
Immediately after this, gws drive files list succeeded and returned real results.
Suggested fix
gws auth login (or a new --fresh/--clear-cache flag) should fully clear all local token/credential state before starting a new interactive login, rather than leaving a possibly-stale secondary cache (token_cache.json) in place alongside a freshly-written credentials.enc. Right now the only way to force a truly clean re-auth is to know to manually delete files inside ~/.config/gws/ — there's no documented or CLI-exposed way to do this.
Happy to provide more detail if useful — this took a couple hours of systematic elimination to track down, so hopefully this saves someone else the trouble.
Activity
ryangu00 commentedon Jul 31, 2026
Reproduced on 0.22.5 (macOS 25.5, keyring backend) with a discriminator that pins the mechanism down to the token cache file, plus a validated workaround.
Scenario. Existing grant without
gmail.settings.basic; rangws auth login --scopes …gmail.settings.basic,gmail.modify,gmail.labels…, consent completed,auth statusimmediately showed the new scope set, andcredentials.encgot a fresh mtime.Split behavior, same grant, same minute:
gws gmail users settings filters list→ works (proves the new grant is live)gws gmail users settings filters create→ 403 “Request had insufficient authentication scopes”, persistently (retried over ~20 minutes; not TTL-flavored)Control experiment — took
refresh_tokenfromgws auth export --unmasked, minted an access token directly againstoauth2.googleapis.com/token, and calledPOST /gmail/v1/users/me/settings/filtersraw: succeeds first try. So the grant is fine; only the CLI’s own token path is serving something stale for some calls.File-level evidence:
gws auth loginrefreshed the credentials but did not invalidate~/.config/gws/token_cache.json.Workaround (validated):
After removing the cache file, the exact same
filters createinvocation succeeded immediately.Suggested fix: invalidate (or namespace-by-grant)
token_cache.jsonwheneverauth loginwrites new credentials — a stale cache surviving a re-auth is what produces the “some calls work, some 403” split reported here. Happy to turn this into a PR if that direction looks right to the maintainers.MrMalkio commentedon Aug 25, 2026
Independent confirmation on a different platform and keyring backend — this isn't macOS/keyring-specific.
Environment: Windows 11, gws 0.22.5,
GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file(socredentials.enc, not the OS keyring).Scenario: existing 14-scope grant; ran
gws auth login --scopes …adding Chat and Admin SDK scopes. Consent completed,auth statusimmediately reported all 22 scopes withtoken_valid: true.Result — no split in our case; every newly-scoped call failed:
gws admin-reports activities list→ 403 insufficient authentication scopesgws chat spaces list→ 403 insufficient authentication scopesFile-level evidence:
Worth flagging that our cache had a newer mtime than
credentials.encand still served the stale token — so mtime alone isn't a reliable tell for whether the cache is poisoned. The prior access token was ~11 minutes into its hour, consistent with reuse rather than re-mint.Same workaround, same result: moving
token_cache.jsonaside made both failing calls succeed on the next invocation, no re-consent needed.One data point on impact: we found a
token_cache.pre-drive-gmail.bakin the same config dir from a month earlier — a previous encounter with this bug, "resolved" by renaming the cache and then never re-authenticating, which left the install with zero stored credentials for a month. The failure mode reads as "the login failed" when the login actually succeeded, which is what makes it expensive.+1 on invalidating the cache in
auth login.jojinkoo commentedon Sep 2, 2026
Mechanism confirmed in the v0.22.5 source, plus a discriminator that explains the "some calls 403, others work" split.
Reproduced on gws 0.22.5 (macOS 26, keyring backend) while adding the Admin SDK Directory read-only scopes to an existing grant with
gws auth login --scopes …. Consent completed,auth statuslisted all 15 scopes andtoken_valid: true,credentials.enchad a fresh mtime — and then, with the same credentials in the same minute:users list(Directory API, reached asadmin-reports users list --api-version directory_v1) → 403 "Request had insufficient authentication scopes", persistentlygroups list,members list→ 200The split lines up with which methods had already been called before the re-login. Reading the code at tag
v0.22.5:crates/google-workspace-cli/src/main.rsL259–265 —select_scope(&method.scopes)picks one scope per Discovery method and passes it toauth::get_token(&scopes).auth.rsL262–270 — the authorized-user path builds ayup_oauth2::AuthorizedUserAuthenticatorwithEncryptedTokenStorage, so a still-valid cached token is served without a refresh.token_storage.rsL114–119 — the cache key is the sorted, joined scope string of that request, i.e. one cache entry per method scope.auth_commands.rs—token_cache.jsonis touched only bytoken_cache_path(),handle_status()andhandle_logout()(L1456ff, which removes it together withsa_token_cache.json). The login path (run_login→handle_login_inner) never references it.So after a re-login that adds scopes, every method whose scope key was already cached under the old grant keeps receiving the old access token until it expires (≤1 h) → 403, while methods first called after the re-login mint a fresh token from the new refresh token → 200. That is exactly the split in the original report and in @ryangu00's comment; the "every newly-scoped call fails" variant in @MrMalkio's case is the same mechanism when those scope keys were already in the cache (e.g. from probes made before re-auth). It also explains why the cache file can have a newer mtime than
credentials.encand still serve stale tokens: the login writes its own scope-set entry, it does not evict the others.Discriminator: with the cache in place,
users listfailed at 14:43:58; aftermv ~/.config/gws/token_cache.json ~/.config/gws/token_cache.json.bakthe identical call succeeded at 14:44:20 with no re-consent, andgroups list,members listanddrive about getkept working. A newtoken_cache.jsonwas written by that call.Suggested fix: have the login handler invalidate
token_cache.json(andsa_token_cache.json) once the new credentials are stored — the same removalhandle_logout()already performs — or add a fingerprint of the refresh token/grant to the cache key so an entry minted under a previous grant can never be served to a new one. Either is a small, local change.stefano-tensortech-co commentedon Sep 11, 2026
We hit this exact symptom on Windows 11,
gwsv0.22.5.Repro:
gws chat spaces list. It failed with403 Request had insufficient authentication scopes.gws auth loginwith--scopes. We passed the three Chat scope URLs directly. We did not use the-sshorthand.gws chat spaces listagain. It failed with the same 403 error.gws auth status. It confirmed the stored credentials hold all three Chat scopes. It showedtoken_valid: true. It showed a refresh token present.token_cache.json->token_cache.json.bak_<timestamp>). We rangws chat spaces listagain. It worked at once. It needed no new consent.This confirms a fourth data point. The stale token cache is the actual cause, not the OAuth grant. PR #808 already fixed this defect. A stale-issue bot closed it automatically, not on its merit. We ask a maintainer to reopen it.
lautou commentedon Sep 30, 2026
Possibly-relevant data point, but flagging the confound honestly rather than claiming a clean repro. On 0.22.5: after recovering from a full credential wipe (separate issue,
-s <service>replacing rather than merging scopes — commented on #673), we hit persistent403 insufficient authentication scopeson bothdrive files list(reason: insufficientPermissions) and a Forms API call (reason: unknown— different reason string for what looked like the same failure class), even thoughgws auth statusconsistently showed the correct full scope list includingdrive/forms.body/forms.responses.readonlythroughout. This persisted across at least one more--scopesre-login. It only cleared after a fullgws auth logout+ fresh login. That's consistent with this issue's stale-token_cache.jsonmechanism — but we changed two things in the same round (also fixed an OAuth consent-screen misconfiguration on our end at the same time), so I can't claim this as an isolated confirmation. Noting it in case the reason-string difference (insufficientPermissionsvsunknown) or the "survives one re-login, cleared by full logout" detail is useful to whoever narrows this further.