用 gws 给 Python 脚本提供 Google OAuth 凭据时的一些注意点
目录
背景与动机#
gws 是一个面向 Google Workspace API 的命令行工具。它的定位很直接:用一套统一的 CLI 去访问 Drive、Gmail、Calendar、Sheets、Docs、Chat 等 API,并且直接输出结构化 JSON。按项目 README 的说法,它是 “built for humans and AI agents”,命令面还是根据 Google 的 Discovery Service 动态生成的。
这里有个事实要先说清楚:它不是 Google 官方正式支持的产品。README 明确写了 “This is not an officially supported Google product.” 所以更准确的说法不是“Google 正式推出了一个新 OAuth 工具”,而是 Google Workspace 生态里出现了一个很实用的开源 CLI,它试图把大量样板代码和认证细节集中到一个工具里。
跟传统的 Google OAuth 接法相比,gws 的好处主要在工程层面。按 Google 官方的 OAuth 2.0 文档 和 Desktop App 流程,自己接一次用户授权通常要处理 OAuth client、浏览器跳转、回调、refresh token 持久化、scope 管理,以及后续 token 刷新。对一个只是想操作 Google Calendar 的脚本或 skill 来说,这些步骤并不复杂,但很分散,也很容易把注意力从“我要调用哪个 Calendar API”转移到“我又把 OAuth 配错在哪里”。
gws 解决的正是这部分重复劳动:
- 它把
auth setup、auth login、auth export这些步骤收成了统一命令,适合本机、CI 和脚本化场景 - 它自带按服务筛 scope 的交互流程,不用自己手拼完整授权 URL
- 它输出结构化 JSON,也能把凭据导出给别的程序继续用
- 它本身就是围绕 Workspace API 设计的,所以拿来操作 Calendar 这类接口时更顺手
这也是我在自己的 skill 里使用它的原因。我的目标不是在 skill 里重新实现一套 Google OAuth 流程,而是尽快拿到一个可复用的用户凭据,然后去调用 Google Calendar API。用 gws 之后,skill 只需要关心两件事:
- 申请对的 scope
- 把导出的凭据交给 Python 代码或后续命令继续使用
文章下面的内容就只保留这一条主线:怎样正确用 gws 拿到 token,并把它稳定地接到 Python 的 Google Calendar 操作里。
一句话总结#
如果你的 Python 脚本只是读取 Google Calendar 和 Drive,我目前更倾向下面这种做法:
gws auth setup
gws auth login --readonly -s calendar,drive
gws auth export --unmasked > ~/.config/my-script/token.json
对应文档:
这里最重要的一点只有一条:-s calendar,drive 只是限制 scope picker 里显示哪些服务,真正决定授权范围的是 --readonly 或 --scopes。这一点以本地 gws auth login --help 为准。
第一步:先把 gws 登录好#
如果你本机已经装了 gcloud,按 gws auth setup 走一次即可。
如果你不想依赖 gcloud,直接按 gws 的 Manual OAuth setup 在 Google Cloud Console 里创建 Desktop app 类型的 OAuth client,然后再执行:
gws auth login --readonly -s calendar,drive
这条命令适合“只读 Calendar + Drive”场景:
--readonly表示请求只读权限-s calendar,drive表示只在 picker 里挑 Calendar 和 Drive 相关 scope
如果你想完全显式地指定 scope,也可以直接使用 --scopes:
gws auth login --scopes \
https://www.googleapis.com/auth/calendar.readonly,\
https://www.googleapis.com/auth/drive.readonly
scope 含义参考 Google 官方文档:
第二步:把凭据导出给脚本#
登录完成后,用 gws 官方文档里的导出方式:
gws auth export --unmasked > ~/.config/my-script/token.json
参考:Headless / CI (export flow)
这一步会把 refresh token、client ID、client secret 等信息导出成 JSON。这个文件最好按敏感凭据来保存,不要提交到仓库。
第三步:Python 里显式传入 scopes#
在 Python 里,我目前更倾向直接使用 google-auth 的 Credentials.from_authorized_user_file 并显式传入 scopes=SCOPES:
from pathlib import Path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
SCOPES = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/drive.readonly",
]
TOKEN_FILE = Path.home() / ".config/my-script/token.json"
creds = Credentials.from_authorized_user_file(
TOKEN_FILE,
scopes=SCOPES,
)
if not creds.valid:
creds.refresh(Request())
这样写有两个好处:
- 你的脚本实际使用哪些 scope,一眼就能看清
- 就算导出的 JSON 里没有
scopes字段,google-auth也能按你传入的SCOPES刷新 token
如果你读取的是 JSON 字典而不是文件,也可以使用 Credentials.from_authorized_user_info;原则一样,仍然显式传 scopes=SCOPES。
常见错误:invalid_scope#
如果刷新 token 时遇到:
invalid_scope: Bad Request
优先检查一件事:登录时申请的 scope,和脚本里实际使用的 scope,是不是同一组 URI。
最常见的错误是:
- 登录时用了
gws auth login -s calendar,drive - 代码里却在用
calendar.readonly和drive.readonly
在 Google OAuth 里,calendar 和 calendar.readonly 是不同的 scope;drive 和 drive.readonly 也是不同的 scope。对应关系看官方 scope 文档:
所以对只读脚本来说,通常更省事的做法还是重新登录一次:
gws auth login --readonly -s calendar,drive
gws auth export --unmasked > ~/.config/my-script/token.json
最后保留一条经验#
如果你是给 Python 脚本接 gws,我觉得先记住这条就够用了:
登录时用 --readonly 或 --scopes 把权限说清楚;Python 里再把同一组 SCOPES 显式传给 google-auth。