SSO 我调你老味 06 (a): 授权有四种写法,你知道么?

白白是一名现居上海市的人士。在本文中,她开始研究起了 OAuth 2.0 的理论知识。

本文主要研究 OAuth 2.0 文档中记载的「理论」,不涉及实际上的项目逻辑。

前言

小白找到了深谙 RFC 6749 之道的小黑。在看到 Ta 的时候,小黑正穿着长衫、吃着茴香豆,问她:「OAuth 2.0 授权有四种写法,…你知道么?」

术语的解释

  • Resource Owner (资源所有者): 有资格访问受保护的资源的实体。在接下来的系列文章中,我们称 Resource Owner 为 RO 或者 所有者
  • Resource Server (资源服务器): 托管受保护的资源的服务器。它可以接受对于受保护资源的访问请求,并且基于 Access Token 对其进行反应。
  • Client (客户端): 在所有者的允许下,代其发送对受保护资源请求的应用程序。
  • Authorization Server (授权服务器): 在成功认证所有者并且得到授权的情况下,向客户端签发 Access Token 的服务器。

对于一个客户端,我们说它是:

  • 「公开的」(public),如果它不能保持其认证信息的保密性,且不能通过其他方式保护客户端认证。例如:在资源所有者的设备上运行的客户端 (安装的原生软件,或基于浏览器的软件)。举例而言,GMail Web App 是「公开」的客户端,因为它的源代码是用户可见的,并且相关的保密信息是可以被用户读取的。
  • 「私密的」(confidential),如果它可以保持其认证信息的保密性,或可以通过其他方式保护客户端认证。例如:在某个安全的服务器运行的客户端。因为它们的源代码对用户不可见,并且相关的保密信息也存储在这个安全的服务器上,而不能直接被用户读取。

而 RFC 6749 主要考虑下列三种客户端应用程序:

  • 网络应用 (web application):在网络服务器上运行的私密客户端。资源所有者通过其设备上 user-agent 渲染的 HTML 用户界面访问这一客户端。客户端的认证信息只存储在网络服务器上,并不对资源所有者暴露,资源所有者亦不可读取。

有点难理解?考虑一个采用 SSR (Server-Side Rendering,服务器端渲染) 的客户端即可。用户可以读取的永远只有服务器返回的 HTML 代码,Access Token 和 Refresh Token 只存储在服务器上。

  • 基于 User-Agent 的应用 (user-agent-based application): 在 user-agent (例如浏览器) 内运行的公开客户端。在这种情况下,客户端从一个网络服务器下载客户端代码,并且在 user-agent 中运行。协议数据和认证信息对于资源所有者是容易获取的 (通常也是可见的)。
  • 原生应用 (native application): 一种安装并运行在资源所有者设备上的公开客户端。协议数据和认证信息可被资源所有者获取,且假定应用中嵌入的所有客户端认证信息均可被提取。不过,动态签发的认证信息 (例如 Access Token 和 Refresh Token) 可获得可接受的保护水平。至少,这些凭证受到保护,不会被应用可能交互的恶意服务器获取;在某些平台上,它们还可能受到保护,避免被同一设备上的其他应用程序获取。

翻译尽量贴近原意。最后两句的原文是:At a minimum, these credentials are protected from hostile servers with which the application may interact. On some platforms, these credentials might be protected from other applications residing on the same device.

注意

Authorization Server 的定义原文是 the server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization., 我感觉 authenticateauthorize 两个字不是很好翻译,所以只能尽量把我自己理解的内容放上来了(

在接下来的行文中,原文所有的 authenticate 及其派生词对应认证,而所有的 authorize 及其派生词对应授权

示例

注意:虽然示例中提到了「小白使用账户和密码认证自己的身份」,但是这个认证过程并不涉及 OAuth 2.0。认证是由授权服务器自行部署的。也就意味着:如果愿意的话,授权服务器可以通过 IP 号段,端口,甚至是当天的 Wordle 完成身份认证。OAuth 2.0 在设计上本身是一个 授权框架。对于身份认证,需要用到基于 OAuth 2.0 的 OIDC (OpenID Connect)。

让我们通过一个简单的例子对这四个概念进行解释。

小白想要将自己的 Google 邮箱关联到手机上的第三方邮件管理软件 (例如安卓端的 K-9 Mail 或 iOS/iPadOS 自带的「邮件」软件)。

显然地,小白是资源所有者,Google 的服务器是资源服务器(存储着小白 Google 账户里的邮件),这个第三方邮件管理软件是客户端

Google 另外架设了一个授权服务器,认证和授权的步骤分别如下:

  1. 小白访问 accounts.google.com,在这里,小白输入自己的账户和密码 (又或者是使用指纹、面容识别等方式) 认证自己的身份。
  2. 授权服务器将小白提供的信息与服务器中保存的信息进行对比,验证通过,这里,服务器已经证明了「来者是小白」
  3. accounts.google.com 提示 你是否允许 XXX Mail 访问你的邮件?,问小白要授权
  4. 小白按下,完成授权
  5. 这下,Google 的授权服务器就会向客户端发送 Access Token 了。

认证

Authorization Code (授权码) 模式

RFC 6749 §4.1 用如下五步描述了授权码模式的认证流程:

Step A

客户端将资源所有者指引到认证端点 (原文是 authorization endpoint)。

一般情况下,这直接通过构建一个 URI 完成,它大概长这样:

1
idp.example.org/authorize?response_type=code&client_id=1234567&state=xyz&redirect_uri=https%3A%2F%2F...

这里包括以下几个部分:

  • idp.example.org/authorize: 授权服务器的 URI。
  • query 部分:
    • response_type: 必需,如果使用授权码模式,那么这个值必须为 code
    • client_id: 必需,用户标识符。
    • state: 推荐,一个不透明的 (opaque) 值,用来防止 CSRF 攻击。
    • redirect_uri: 可选,定义认证流程 (无论成功与否) 结束后跳转到的 URI。

至于 CSRF 攻击的流程大概是什么,可以参考 这里

Step B

授权服务器认证所有者,并且确认所有者的授权结果 (同意/不同意)。

Step C

假定所有者同意授权,则授权服务器将客户端重定向回 redirect_uriredirect_uri 中将会包含一个认证码和任何已经给出的 state

Step D

客户端使用上一步得到的认证码,向授权服务器的 token 端点申请一个 Access Token。在提交申请时,客户端会与授权服务器请求认证。客户端会在请求中包含 redirect_uri 作为验证。

Step E

授权服务器认证客户端,验证认证码,并且确定 (C)、(D) 两步提供的 redirect_uri 相符。如果这些都确认成功了,那么返回 Access Token,视情况还会返回一个 Refresh Token。

Implicit (隐式) 模式

注意:目前的最佳实践 ( RFC 9700 §2.1.2 )建议直接弃用 Implicit 模式,而使用 Authorization Code 模式!

使用隐式认证可以直接获取 Access Token,但不支持签发 Refresh Token。

与上文介绍的基于认证码的认证流程不同,使用隐式认证的客户端可以直接从授权服务器获取 Access Token。

RFC 6749 §4.2 用如下七步描述了授权码模式的认证流程:

注:这里的「客户端」本身是托管在网上的,而 user-agent 是本地的。

注:原文涉及到了一个叫做 web-hosted client resource 的概念,因为中文已经用掉了「客户端」的词汇,于是在不改变原意的情况下做出了少许改动。

下在中文语境中引入「客户端的服务器」。用一个例子来解释:

某人使用网页版 GMail,GMail 本身是客户端 (OAuth 语义下的 client),用户的浏览器是 user-agent;而 GMail 本身有一个服务器,储存着 GMail 网页所需的资源,这就是「客户端的服务器」。

Step A

客户端通过将 user-agent 将资源所有者指引到认证端点。

一般情况下,这也直接通过构建一个 URI 完成。URI 的 Query 部分包括以下几个部分:

  • response_type: 必需,值为 token
  • client_id: 必需,用户标识符。
  • state: 推荐,同上文 state 部分。
  • scope: 可选,定义访问请求的权限范围。
  • redirect_uri: 可选,同上文 redirect_uri 部分。

Step B

授权服务器认证所有者,并且确认所有者的授权结果 (同意/不同意)。

Step C

假定所有者同意授权,则授权服务器将客户端重定向回 redirect_uriredirect_uri 中将会在 URI fragment 处包含 Access Token。

URI fragment 就是 URI # 号后面的东西。比如,使用隐式模式作为认证的授权服务器可能给客户端返回这个 URI:

1
https://app.some-service.com/callback#access_token=TheAccessToken&...

Step D

客户端通过向客户端的服务器发出一个请求,将所有者重定向到 (C) 步返回的重定向 URI 处。user-agent 在本地保存 URI fragment 中的内容。在重定向的过程中,URI fragment 中的内容并不包含在请求中。

Step E

(由于客户端部署在网上,) 客户端的服务器返回一个可以获取完整重定向 URI,并且可以从 URI fragment 中提取 Access Token 的网页 (一般是有嵌入代码的 HTML 文件)。

(E) The web-hosted client resource returns a web page (typically an HTML document with an embedded script) capable of accessing the full redirection URI including the fragment retained by the user-agent, and extracting the access token contained in the fragment.

Step F

user-agent 在本地执行客户端的服务器给出的代码,提取 Access Token。

Step G

user-agent 将 Access Token 传递给客户端。

Resource Owner Password Credentials (资源所有者密码) 模式

都不用等到 RFC 9700, RFC 6749 本身就已经声明了这个模式「只应该在别的认证流不可用时使用」(only allow it when other flows are not viable)。

使用该模式也可以使授权服务器直接返回 Access Token 和 Refresh Token (如可行)。

Step A

资源所有者向客户端提供其用户名和密码。

一般情况下,这也直接通过构建一个 URI 完成。URI 的 Query 部分包括以下几个部分:

  • grant_type: 必需,值为 password
  • username: 必需,用户名。
  • password: 必需,密码。
  • scope: 可选,定义访问请求的权限范围。

Step B

客户端向授权服务器的 token 端点请求一个 Access Token。请求中会包含资源所有者的认证信息 (credentials)。在请求的同时,客户端与授权服务器发生认证。

Step C

授权服务器认证客户端并且验证资源所有者的认证信息。如果认证信息是合法的,则签发 Access Token。

Client Credentials (客户端认证信息) 模式

当客户端请求的受保护资源是受客户端本身管控的,或者虽然属于其他所有者,但已提前经受授权服务器处理的,则客户端可以直接使用其客户端认证信息向授权服务器请求一个 Access Token。

Step A

客户端与授权服务器进行认证,并从 token 端点请求一个 Access Token。

一般情况下,这也直接通过构建一个 URI 完成。URI 的 Query 部分包括以下几个部分:

  • grant_type: 必需,值为 client_credentials
  • scope: 可选,定义访问请求的权限范围。

Step B

授权服务器认证客户端。如果认证成功,签发一个 Access Token。

以 CC BY-NC-SA 4.0 许可证分发