☰
Operit GitHub OAuth Broker 云端交换协议:PKCE 短期认证事务与一次性领取凭据实战解析
2026/9/27 7:40:36 网站建设 项目流程
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

导读

本文围绕 Operit 项目中docs/TODO/github_oauth_broker/1_CloudBroker.md所定义的云端交换协议展开,讲解 Operit 如何把 GitHub OAuth 的授权码交换从 Android 设备端迁移到api.operit.app的受保护 Worker,并用 PKCE verifier、一次性领取凭据(claim credential)与短期认证事务构建"设备不接触 client secret、敏感凭据不落地浏览器 URL"的登录链路。读完本文,你将掌握该协议的事务创建、完成回调、单次领取三阶段设计,以及它在 Android 与 Operit 2(Rust CLI / Flutter)两代客户端中的落地方式,并可直接对照仓库源码复现每一条安全边界。

背景:client secret 为何不能留在 APK 里

在进入新协议之前,先看旧实现为什么必须被替换。docs/TODO/github_oauth_broker/index.md记录了这次改造的直接动因:

Android 客户端曾将 GitHub OAuth client secret 写入 BuildConfig,并在设备上直接交换授权码。该 secret 随已发布 APK 分发,不能继续作为可信凭据。

这是移动端 OAuth 的经典悖论:APK 可以轻易被反编译,编入 BuildConfig 的 client secret 等同于公开;同时授权码在设备端换取 token,意味着任何能读到 APK 的人都能冒充客户端完成同样的交换。旧协议里"设备向 GitHub 直接提交 OAuth 授权码和编入 APK 的 client secret"(见 1_CloudBroker.md)因此被判定为不可继续使用。

改造目标被明确写进 index.md:

  • 新版 Android 只通过api.operit.app的受保护 Worker 完成 GitHub 授权码交换;
  • 新 APK 不再包含 client ID、client secret 或operit://OAuth 回调;
  • 已发布的旧 APK 继续使用原 OAuth App,直至发布公告规定的迁移截止日;
  • 新 OAuth App 的 client secret 只存在于 Cloudflare Worker secret。

其中最后一条是整套协议的安全基石:secret 只存在于云端,设备端永远拿不到。

旧实现:凭据落地的三种路径

1_CloudBroker.md与配套的 2_AndroidClient.md、3_Operit2Client.md 共同勾勒了改造前的全貌,主要有三条路径:

客户端旧实现方式安全问题
AndroidGitHub 登录界面提供内嵌 WebView 与外部浏览器两条路径,应用接收operit://github-oauth-callback后直接向 GitHub 交换 token授权码经自定义 scheme 回传,client secret 在设备上参与交换
Rust CLI(Operit 2)使用 GitHub Device Flow,并要求操作者设置 GitHub OAuth client ID 环境变量凭据进入环境变量,流程与浏览器式授权割裂
Flutter 市场页(Operit 2)要求用户创建并粘贴 GitHub Token用户长期令牌被手动粘贴进客户端,风险面大

旧 Android 实现的另一个隐患是自定义 scheme 与 Activity Intent 接管:operit://github-oauth-callback这样的 scheme 属于全局可声明标识,存在被其他应用抢占的风险,同时浏览器回调 URL 会真实携带授权码。新协议的目标之一就是把"授权码、token 和领取凭据"全部排除在浏览器回调 URL 之外(见 1_CloudBroker.md 的结果清单)。

新协议核心:短期认证事务三阶段

新实现的协议骨架定义在 1_CloudBroker.md:

Worker 创建短期认证事务,生成 PKCE verifier 与一次性领取凭据。GitHub 回调由 Worker 接收并交换授权码,用户 token 加密暂存后重定向到浏览器回调 Host 预先注册的完成地址;Core 校验完成链接后以领取凭据 claim 一次,记录随即删除。

把这段话拆解为三个阶段的时序:

  1. 事务创建(start):客户端向 Worker 提交自己准备的回调完成地址(completion redirect URI),Worker 创建短期认证事务,生成 PKCE verifier 与一次性领取凭据(delivery credential),返回授权页面 URL、事务 ID、凭据与过期时间;
  2. 授权与回调(complete):用户浏览器访问 GitHub 授权页,授权码回调由 Worker 接收并完成 PKCE 交换,获取的 token 在 Worker 侧加密暂存,随后把浏览器重定向到客户端预注册的完成地址,完成链接中只携带事务状态;
  3. 单次领取(claim):客户端(Core)校验完成链接与当前事务匹配后,用一次性领取凭据 claim 一次,Worker 返回 token 与用户信息,服务端记录随即删除。

这套设计同时满足四个约束(对应 1_CloudBroker.md 的"结果"):

  • client secret 不离开 Cloudflare secret(不落入 APK、不进入请求体);
  • 授权码、token 和领取凭据不出现在浏览器回调 URL 中(回调 URL 只携带transactionId与status等非敏感参数);
  • 完成通知不产生 Worker 轮询请求(客户端一次 start、一次 claim,没有状态轮询);
  • 新接口不改动旧客户端使用的/market/v2/auth/github(市场旧接口保持兼容)。

协议责任划分(3_Operit2Client.md 的"协议责任")进一步明确了边界:

  • Worker 持有 OAuth client secret、生成 PKCE 和处理 GitHub 回调;
  • 客户端不包含 client ID 或 client secret;
  • Flutter 市场页不持有 OAuth HTTP、平台 Intent、EventChannel 或 loopback receiver,只管理自己的可见 WebView 导航;
  • 客户端不向 Worker 反复查询授权状态;
  • Rust 解析和校验 Broker 响应,并以单元测试固定协议契约。

客户端协议实现:Broker Service 与 Coordinator

云端 Worker 的行为无法在本仓库直接查看(后端位于独立的marketWorker 工程),但 Android 端的协议实现完整存在于本仓库,可以直接对照。

1.GitHubOAuthBrokerService:协议的两个 HTTP 端点

GitHubOAuthBrokerService.kt 是客户端侧与 Worker 通信的唯一入口,基地址硬编码为https://api.operit.app(见 L144)。它封装了两个请求:

  • startLogin(completionRedirectUri)(L60-L82):POST$BROKER_BASE_URL/oauth/github/start,请求体为{"completionRedirectUri": "..."},解析返回GitHubOAuthBrokerStartResponse,该响应携带transactionId、deliveryCredential、authorizationUrl、completionRedirectUri与expiresAt(L18-L25)。客户端拿到后即可展示授权页,同时本地私有保存领取凭据;
  • claimLogin(transactionId, deliveryCredential)(L84-L128):POST$BROKER_BASE_URL/oauth/github/claim,请求体为{"transactionId": "...", "deliveryCredential": "..."}。响应status必须为complete,否则拒绝(L108-L112),随后解出accessToken、tokenType、scope、expiresIn、refreshToken与user(L113-L122)。

两个请求都使用 30 秒超时的 OkHttpClient(L49-L53),错误统一封装为IllegalStateException,并附带 HTTP 状态码与响应体便于排查(L130-L136)。响应解析使用ignoreUnknownKeys的宽松 Json 配置(L55-L58),保证前后端字段演进时旧客户端不因多余字段崩溃。

2.GitHubOAuthCoordinator:事务生命周期管理

GitHubOAuthCoordinator.kt 是客户端侧的编排中枢:

  • startLogin(completionRedirectUri)(L18-L33):调用 Broker Service 创建事务,并把transactionId、deliveryCredential、expiresAt通过GitHubAuthPreferences.saveActiveOAuthTransaction写入本地 DataStore,随后返回事务供 UI 展示授权页;
  • completeLogin(completionUri)(L35-L82):先校验完成链接的transactionId与当前活动事务一致(L39-L41),否则直接失败;随后按status参数分流——complete继续领取、denied视为用户取消并清事务、error透传错误信息、其余视为非法状态;确认完成后调用claimLogin领取一次 token,保存认证信息并清除活动事务;
  • cancelLogin()(L84-L86):用户取消时清空活动事务,避免遗留凭据被复用。

值得注意的细节:内嵌登录使用固定完成地址https://api.operit.app/oauth/github/complete(L90),而外部浏览器登录则使用 loopback 临时地址(见下文),两者都会在 start 时预注册给 Worker,对应文档中"重定向到浏览器回调 Host 预先注册的完成地址"。

3. 事务凭据的持久化:GitHubAuthPreferences

GitHubAuthPreferences.kt 基于 DataStore(github_auth_preferences,L19-L20)管理全部 GitHub 认证状态。与本次改造直接相关的设计有:

  • 认证版本门槛:REQUIRED_AUTH_VERSION = 3(L43),isAuthSessionCurrent(L90-L94)要求本地会话的auth_version >= 3且授予 scope 覆盖notifications,public_repo,user:email,read:user(L42),旧版本认证数据不会被新认证代码继续使用,呼应 2_AndroidClient.md 的"认证版本升级,旧 APK 数据不会被新版认证代码继续使用";
  • 活动事务三键:active_oauth_transaction_id、active_oauth_delivery_credential、active_oauth_expires_at(L55-L57),getActiveOAuthTransaction(L245-L258)在读回时会检查过期并自动清除,杜绝过期凭据被 claim;
  • 领取后清理:saveAuthInfo(L131-L165)在写入 token 的同时移除活动事务三键,与服务端"记录随即删除"形成两端对称的单次语义。

Android 登录 UI:通用浏览器回调组件与双路径

GitHubLoginDialog.kt 保留了"内嵌 WebView"与"外部浏览器"两条登录路径(L50-L54 的GitHubLoginMode三态:CHOOSER / EMBEDDED / EXTERNAL),但底层机制全部替换。

内嵌路径:BrowserCallbackDialog通用组件

内嵌登录不再持有 GitHub 协议逻辑,而是复用通用组件 BrowserCallbackDialog.kt(注释明确写着 "Presents one host-owned browser flow and reports navigation to its registered callback destination")。它只负责三件事:

  1. 加载authorizationUrl(L109-L111);
  2. 在shouldOverrideUrlLoading与onPageStarted两个时机捕获导航(L79-L95),用matchesCallbackDestination(L189-L194)按scheme / host / port / path 四元组匹配完成地址,命中即回调onCompletion(uri)并stopLoading();
  3. 处理超时与释放:expiresAt到期未完成则回调onFailure(L113-L127),releaseBrowserCallbackWebView(L197-L208)在释放时依次执行停止加载、about:blank、清历史、移除视图、destroy(),避免 WebView 泄漏。

由于完成地址是https://api.operit.app/oauth/github/complete这样的 https 地址而非自定义 scheme,2_AndroidClient.md 的"结果"清单中的三项随之成立:删除旧自定义 scheme、外部浏览器回调和 Activity Intent 接管;浏览器回调组件不包含 GitHub 协议、token 或领取凭据;删除 Android 的 client ID 与 client secret BuildConfig 字段。

外部路径:一次性 loopback 接收器

外部浏览器登录使用 GitHubOAuthLoopbackCallbackServer.kt 在127.0.0.1上临时监听一个端口(要求端口号 >= 1024,L98、L112-L114),完成地址为http://127.0.0.1:<port>/oauth/github/complete(L15-L22)。awaitCompletion(L24-L40)只接受一次 GET 请求:校验路径与完成地址一致(L63-L66),把查询参数拼接回完成 URI 后返回 200,其余请求返回 404。整个流程在 GitHubLoginDialog.kt 的GitHubExternalLoginDialog中(L240-L301)用withTimeout(remainingMillis)包裹,超时即按登录失败处理,finally中关闭服务器并清理事务——这与文档"完成通知不产生 Worker 轮询请求"的约束一致,因为整个链路只有一次授权页展示和一次完成回调。

登出与会话隔离

2_AndroidClient.md 还规定了一个易被忽略的体验细节:

用户明确退出 GitHub 登录时,应用会清除自身 WebView 的 Cookie 和 WebStorage,再删除本地认证信息。下一次登录不会静默复用之前的 GitHub Web 会话;这不会影响系统浏览器或 Chrome 的 GitHub 登录状态。

即退出登录需要同时清 WebView 会话与本地认证数据,但作用域严格限定在应用自身 WebView,避免误伤系统浏览器中用户已登录的 GitHub 账号。

Operit 2 客户端迁移:类型化服务替代命令字符串

Operit 2(Rust CLI 与 Flutter 市场)的迁移方向与 Android 一致,但多了一个架构约束:3_Operit2Client.md 要求两端都调用 Core 的类型化GitHubOAuthBrokerService,Flutter 使用生成的 Dart proxy、CLI 使用生成的 Rust proxy,"两者都不传递市场认证命令字符串、不解析命令 stdout,也不手写 CoreLink 请求"。

具体分工:

  • 应用自己准备完成地址,Core 用该地址调用 Worker 的/oauth/github/start并私有保存 delivery credential;
  • 应用展示授权页并交回完成链接——CLI 使用临时 loopback 并在终端打印授权链接,Flutter 市场登录对话框拦截其 WebView 的完成导航;
  • Core 只在收到与当前事务、目标地址都匹配的完成链接后 claim 一次并保存 Worker 返回的 GitHub token;
  • Rust 侧通过单元测试固定协议契约("Rust 解析和校验 Broker 响应,并以单元测试固定协议契约")。

这条迁移路径的意图很清晰:旧实现里 CLI 依赖环境变量 client ID、Flutter 要求用户粘贴 token、两端以命令字符串与 stdout 解析方式对接市场认证,都属于"凭据或协议细节外泄"的脆弱设计;新实现把协议收敛为类型化的 start/claim 调用,凭据只在 Core 与 Worker 之间传递。

结果清单与安全收益汇总

综合 1_CloudBroker.md 与 2_AndroidClient.md 的结果章节,本次改造的验收标准如下:

  • client secret 不离开 Cloudflare secret,任何客户端二进制与请求体中都不可见;
  • 授权码、token 和领取凭据不出现在浏览器回调 URL 中,回调 URL 仅携带事务 ID 与状态;
  • 完成通知不产生 Worker 轮询请求,全链路仅 start / claim 两次 HTTP 往返;
  • 新接口不改动旧客户端使用的/market/v2/auth/github,旧接口保持兼容直至迁移截止日;
  • Android 删除旧自定义 scheme、外部浏览器回调和 Activity Intent 接管,浏览器回调组件不含 GitHub 协议、token 或领取凭据;
  • Android 删除 client ID 与 client secret 的 BuildConfig 字段,认证版本升级到 3,旧 APK 数据不会被新认证代码继续使用。

从实现事实看,这些收益都能在本仓库的源码中得到印证:GitHubOAuthBrokerService的请求体只有completionRedirectUri/transactionId/deliveryCredential(GitHubOAuthBrokerService.kt),没有任何 client secret 字段;完成链接校验依赖transactionId与状态参数(GitHubOAuthCoordinator.kt);仓库中已搜不到operit://github-oauth-callback或 client secret BuildConfig 字段的残留。

部署顺序与现状

index.md 的"状态"章节记录了该改造的推进节奏:云端密钥已配置、新 OAuth App 已创建、D1 迁移已应用;Worker 部署待后端现有未提交市场改动整理后执行;Operit 2 客户端迁移进行中(CLI 包当时的编译问题与本协议无关)。部署顺序明确为后端先于 Android——这是合理的依赖顺序:新 APK 依赖 Worker 的/oauth/github/start与/oauth/github/claim端点,云端必须先就绪,旧客户端才能继续使用原 OAuth App 平稳过渡到迁移截止日。

对读者而言,若要在自己的项目里复刻这套方案,最小可复制的骨架是:一台持有 client secret 的云端 Worker(负责 PKCE 与授权码交换)、一个短期事务存储(带过期与单次领取语义)、客户端侧一次 start + 一次 claim 的类型化调用,以及一个只按 scheme/host/port/path 匹配完成地址的通用浏览器回调组件。Operit 的 GitHubOAuthBrokerService.kt 与 GitHubOAuthCoordinator.kt 提供了现成的参考实现。

  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:深入解析mshumer/gpt-author项目:AI自动生成小说全流程指南
下一篇:从0到1:使用Google Workspace MCP Server构建自动化邮件处理系统

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询