OAuth 流程图指南

怎么画一张真正有用的 OAuth 流程图:四个参与方、前后通道的区别,以及三个可直接改的例子 —— PKCE、刷新令牌轮换、机器对机器。

发布于 ·10 分钟阅读
oauthsequence-diagramsecuritytemplate

一、什么是 OAuth 流程图

OAuth 流程图 画的是:用户如何让一个应用访问自己在另一个服务上的数据,而不用把密码交出去。图上有四方,以及它们对话的顺序 —— 资源所有者(人)、客户端(你的应用)、授权服务器(发令牌的)、资源服务器(存数据的 API)。

把这四方叫对,就已经是这张图大半的价值。OAuth 图最常见的错误是把授权服务器和资源服务器并成一个叫「Google」的框。它们是两个系统、两份职责:一个决定 你是谁、你能干什么,另一个 提供数据、校验拿到的令牌。并成一个框之后,这张图就再也画不出最关键的那件事:谁会看到用户的密码(只有前者),谁自始至终只见过令牌(只有后者)。

第二个理由是 OAuth 是跳转协议,而跳转在代码里是隐形的。读一个 handler 只能知道某个端点发生了什么,读不出浏览器在三个源之间弹了三次、还带着一个必须原样返回的 state。时序图能把这几次弹跳画出来 —— 这就是为什么几乎所有真实的 OAuth bug(redirect_uri 对不上、state 丢了、code 被重放)在图上一目了然,在调用栈里完全看不见。

如果你看过 RFC 里那张图却还是画不出自己那版,这很正常:规范画的是抽象协议,你的图需要的恰好是规范留作练习的那些部分 —— 也就是下面三个例子。

二、一张 OAuth 流程图要画出哪些东西

五样。第一样就是「有用的 OAuth 图」和「照抄规范插图」的分水岭:

  • 哪些箭头经过浏览器。 OAuth 有 前通道(用户浏览器跟着走的跳转,地址栏里看得见、历史记录里留得下)和 后通道(服务器之间的调用,谁也看不见)。协议里所有的设计决策都源自这个区分。一定要标出来 —— 虚线、注释、颜色,什么都行。
  • 每条箭头带的是什么。 codeaccess_tokenrefresh_tokenid_token 是四样东西,四种生命周期、四种泄露后果。每条箭头都写「token」,等于把整个安全叙事全丢了。
  • 授权服务器和资源服务器分成两条生命线,哪怕它们是同一家厂商在运营。理由见上一节。
  • 至少一条拒绝分支。 code 过期、state 对不上、PKCE 校验不过、刷新令牌被吊销 —— 挑你们团队真的会争论的那一条,用 alt 画出来。
  • 令牌最后落在哪。 停在「签发了 access token」的图只回答了一半问题。有意思的是另一半:客户端拿它怎么办 —— 放内存、放 cookie,还是放在一个不该放的地方。

这是骨架 —— 授权码流程,没有错误分支,四个参与方各占一条线。下面每个例子都是在它上面加一样具体的东西。

看 Mermaid 源码
sequenceDiagram
    participant User as Resource Owner
    participant App as Client App
    participant Auth as Authorization Server
    participant API as Resource Server

    User->>App: click Sign in
    App->>Auth: redirect to /authorize
    Auth->>User: login and consent screen
    User->>Auth: approve
    Auth-->>App: redirect back with code
    App->>Auth: POST /token with the code
    Auth-->>App: access token
    App->>API: GET /me with the access token
    API-->>App: profile data
最小的 OAuth 授权码流程图,四个参与方:资源所有者、客户端应用、授权服务器、资源服务器。

三、OAuth 流程图怎么画

三步。大部分图错在第 1 步,而那一步只需要两分钟。

第 1 步 · 先确定你画的是哪个流程

「OAuth」不是一个流程,画一个笼统的版本,结果是对每个具体场景都不对。先回答一个问题 —— 签发令牌的那一刻,有人在场吗?

有人在场,你画的是 授权码 + PKCE。Web 应用、单页应用、移动端都是这个答案,自从 PKCE 对公开客户端成为强制要求以来一直是。如果你看到某篇教程画的是 隐式流程(令牌直接跟在跳转 URL 里返回),那是那之前的东西,不要抄。

没人在场 —— 定时任务、跟合作方 API 同步的服务 —— 你画的是 客户端凭据,图上根本没有用户那条生命线。这个「没有」本身就是信息:它解释了为什么没有授权页、也没有刷新令牌。

其他的(电视机上的设备码、服务之间的令牌交换)是你需要时自然会知道的变体。挑一个,只画那一个。一张图同时覆盖两个流程,等于两个都没覆盖。

第 2 步 · 把前通道和后通道分开

把箭头过一遍,分成两堆:浏览器带着走的我们服务器直接调的

分类本身是机械的 —— 凡是跳转、凡是会出现在用户地址栏里的 URL、凡是用户提交的表单,都是前通道 —— 但分完的结果就是这个协议的全部要点。前通道是可观察的:它在浏览器历史里、在 Referer 头里、在服务器日志里,还可能就在一台共用电脑的屏幕上。这正是为什么授权码走前通道而访问令牌不走。授权码短命、一次性、没有客户端密钥或 PKCE 校验值就没用 —— 它就是被设计成「被看见也没关系」的。

分完之后检查一件事:前通道上有没有什么东西,被身后的人瞄一眼就会出事? 有的话,那就是一个发现 —— 而这张图存在的意义就是产出这种发现。

第 3 步 · 画出来

把这套握手用句子描述出来,让 text2diagram 排版 ——「应用带着 code challenge 把浏览器跳转到授权服务器,用户登录并同意,服务器带着 code 跳回来,应用拿 code 加校验值去令牌端点换令牌」,还回来的是一段可以直接改的 sequenceDiagram

或者把下面的例子在编辑器里打开,把参与方改成你的服务商。难的是箭头怎么排,Authorization ServerOkta 只是一次查找替换。

四、三个 OAuth 流程图例子

三个规范插图不会替你画的流程:现在的默认做法、令牌过期之后发生什么、以及完全没有用户时是什么样。

例 1 · 授权码 + PKCE

值得琢磨的是第一条和最后一条箭头。应用生成一个 code_verifier,只把它的 哈希(code_challenge)通过前通道发出去;到最后一步才把校验值本身通过后通道发过去。从跳转里偷到 code 的攻击者有 code 没有校验值,这次交换就失败 —— 这就是全部构思,而它在图上是看得懂的,在文字里不是。

还要注意 check state matches 那条自箭头。它不是网络调用,所以经常被省略;而省略它正是回调端点上的 CSRF 上线的方式。一个起防护作用的步骤值得有一个框,哪怕它不过网。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant User as Browser
    participant App as Client App
    participant Auth as Authorization Server
    participant API as Resource Server

    App->>App: generate code_verifier and code_challenge
    App->>Auth: GET /authorize with client_id, redirect_uri, code_challenge, state
    Auth->>User: login and consent
    User->>Auth: approve
    Auth-->>App: 302 back to redirect_uri with code and state
    App->>App: check state matches what we sent
    App->>Auth: POST /token with code and code_verifier
    alt verifier does not match the challenge
        Auth-->>App: 400 invalid_grant
    else verifier matches
        Auth-->>App: access_token and refresh_token
        App->>API: GET /me with Bearer access_token
        API-->>App: profile
    end
带 PKCE 的 OAuth 授权码流程图,包含 code challenge 生成、state 校验,以及 invalid_grant 拒绝分支。

例 2 · 刷新令牌轮换与重用检测

这是团队跳过、然后凌晨两点来查的那条路径。访问令牌不停过期,所以这条路比上面的登录跑得频繁得多 —— 而它有一条大多数图都不画的分支。

开了 轮换 之后,每次刷新都作废旧令牌、发一个新的。这就把「刷新令牌被偷」变成一个可检测的事件:如果 RT1 被出示了两次,那么攻击者和真客户端里必有一个在用服务器已经作废的令牌,而服务器分不出是哪个。唯一安全的反应是把整个令牌家族全部吊销、逼用户重新登录。把第二条分支画出来,才让「我们做了刷新令牌轮换」从一个勾选框变成一个有明确后果的行为。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant App as Client App
    participant Auth as Authorization Server
    participant API as Resource Server

    App->>API: GET /orders with Bearer access_token
    API-->>App: 401 token expired
    App->>Auth: POST /token with refresh token RT1
    alt RT1 is unused and valid
        Auth->>Auth: revoke RT1, issue RT2
        Auth-->>App: new access_token and RT2
        App->>API: retry GET /orders
        API-->>App: 200 OK
    else RT1 was already used - it leaked
        Auth->>Auth: revoke the whole token family
        Auth-->>App: 400 invalid_grant
        App->>App: clear session, send the user to login
    end
OAuth 刷新令牌流程图:成功时轮换,检测到重用时吊销整个令牌家族。

例 3 · 客户端凭据 —— 完全没有用户

要注意的是这张图里 缺了什么:没有浏览器生命线、没有授权页、没有跳转、根本没有前通道。这里每条箭头都是服务器到服务器,这既解释了为什么这个流程可以直接用客户端密钥,也解释了为什么 PKCE 在这里无事可做。

值得画的是那条乏味的分支 —— 令牌在一个长批处理中途过期。它一直很乏味,直到某天夜里同步跑了一半,两个系统的数据对不上了,这时候所有人都想知道:这个任务是重新认证接着跑,还是就地放弃?图上一眼就能回答。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant Job as Nightly Job
    participant Auth as Authorization Server
    participant API as Partner API

    Note over Job: no user is present - nobody to consent
    Job->>Auth: POST /token with client_id and client_secret
    Auth-->>Job: short lived access_token, scoped
    Job->>API: POST /sync with Bearer access_token
    alt token still valid
        API-->>Job: 200 OK
    else token expired mid batch
        API-->>Job: 401
        Job->>Auth: POST /token again
        Auth-->>Job: fresh access_token
        Job->>API: retry POST /sync
        API-->>Job: 200 OK
    end
机器对机器的 OAuth 客户端凭据流程图,包含令牌在批处理中途过期后重新认证的分支。

如果有一条箭头带着机密穿过浏览器,那就是这张图的结论

画完之后,只读前通道那几条箭头,逐条问:这一条要是被截图了,代价是什么?授权码 —— 按设计,没有代价。跟在 URL 片段里的访问令牌 —— 一个会话。客户端密钥出现在浏览器附近 —— 全部,而且它说明这个应用是个公开客户端,本来就该改用 PKCE。

这是 OAuth 图能让你廉价做完、而读代码几乎做不到的一次评审:前通道是散在一次跳转、一个回调路由和一个你控制不了的浏览器里的。

常见问题

接着读

去试试 text2diagram

打开工具
← 回到全部教程