05. 通关文牒:OAuth 2 里的访问令牌

在马里奥代替国王营救公主的故事里,如果没有 OAuth 2,为了证明马里奥是代表国王去营救公主,他拥有各个蘑菇城的通行权限,国王不得不将代表自己无上权力的皇冠拿给马里奥,很明显,这于情于理都是不合理的。因此为了解决这个问题,国王授权给马里奥一个有时限的、绑定了对若干蘑菇城通行权限的通关文牒。对应 OAuth 2,蘑菇城的通行权限我们称为 scope,而通关文牒我们则称为访问令牌。这一小节我们将讨论 scope 和访问令牌相关的内容。至于国王如何授权通关文牒给马里奥,这是 OAuth 2 的授权方式定义的内容,我们将很快在后面的小节与之邂逅。

scope 和访问令牌令牌存在的必要性

在 OAuth 诞生之前,如果你想委托第三方应用程序访问你存储在资源服务器上的某些资源,你唯一可选的方式是将您的用户名密码交付给第三方应用程序。这就相当于国王委托马里奥营救公主的时候,为了证明马里奥是代表国王去营救公主,并且有蘑菇城的通行权限,国王直接将王冠交给了马里奥。
但在2006年之前,这差不多是实现授权访问的唯一方式,这在今天看来是不可思议的。例如早期如果你用电子邮箱注册 Twitter,Twitter 会直接向你索要你的邮箱密码,以便它能够读取你的邮箱好友列表进而向他(她)们推荐加入Twitter。这会带来哪些问题呢?
  • 你赋予了第三方应用程序你当前账户的所有权限。你的账户密码就相当于你家里的钥匙,一旦你将钥匙交给其他人,那么他就可以肆无忌惮的随意进出你的家门,并且享受你在家里的一切特权。对应网络世界,一旦第三方应用程序获得了你的账号密码,他就可以进行例如像修改密码,删除你的博客数据等操作。
  • 第三方应用程序会以非安全的方式保存你的密码。在Web安全领域,加密始终是一个严肃的话题。以明文的形式保存用户的密码是绝对不可取的。但是在这种情况下第三方应用程序为了持续保持对你账户的访问,他们必须以明文的形式保存密码。
  • 覆水难收的授权。最开始的时候我们将第三方应用程序视为值得信赖的伙伴,所以我们放心地将密码交付给这些程序,但是未来某一天我们发现它其实是一个恶意的软件,它在偷偷利用你的账户做一些你不允许的事情。在这种情况下你唯一能收回授权的方式是修改密码甚至注销账号!
  • 不断被放大的密码失窃风险。试想一下,即使一开始你认为某一个第三方应用程序是一个值得信赖的伙伴,你可以完全放心地将账号密码交给它。但是如果有一天它被黑客攻击了呢?此时你不得不修改你的密码。你越多的共享你的密码,就越放大了你密码失窃的风险。
 
那么 OAuth 2 是如何解决这个问题的呢?那就是使用权限范围(scope)和访问令牌(access token)。权限范围是 OAuth 2 中限制应用程序访问用户帐户的机制,它定义了客户端可以在资源服务器上执行的操作。在马里奥营救公主的故事中,这就相当于各个蘑菇城的通向权限。而通关文牒相当于 OAuth 2 的访问令牌(access token)。一个访问令牌往往关联和绑定了多个权限范围,它代表了终端用户对客户端的授权结果集,并且这个访问令牌也是有时限和可以撤销回收的。
 
在客户端发送授权请求(我们在第一关的第7小节会介绍授权请求的详细内容)时,客户端指明它期望从终端用户那里获得的权限范围集合,授权服务器并不会直接同意来自客户端的授权集合申请,而是提供了终端用户做授权决策的机会,终端用户可以同意授予对应用程序请求的一个或多个作用域的访问权限,或者拒绝该请求。在令牌响应中,授权服务器必须将访问令牌及令牌关联的作用域返回给客户端应用程序。如下图所示,Google OAuth定义了谷歌日历的scope。
当一个客户端想查看终端用户的日历设置和查看终端用户日历上的所有活动时,完整的交互图如下。
  1. 用户访问客户端程序。
  2. 客户端初始化授权请求,在请求里指明了它期望的权限范围集合。授权请求如下(注意 scope 参数的内容)。
 GET o/oauth2/v2/auth?
 
 redirect_uri=[REDIRECT_URI]
 &response_type=code
 &client_id=[CLIENT_ID]
 &scope=https://www.googleapis.com/auth/calendar.events.readonly
 +https://www.googleapis.com/auth/calendar.settings.readonly HTTP/1.1
 
  Host: https://accounts.google.com/
 
  1. 授权服务器提示终端用户做授权决策。
  2. 终端用户同意客户端携带的所有权限范围。
  3. 授权服务器向客户端返回了访问令牌、刷新令牌以及访问令牌关联的作用域集。
 
在第(2)步中,客户端发送的授权请求中只请求它需要的权限范围集合,这遵循了最小权限原则。最小权限原则规定每个程序和系统用户都应该具有完成任务所必需的最小权限集合,而不应该具有更多权限。可以根据需要添加权限,并在不再使用时撤销权限。遵循最小权限原则尽可能地保护数据以及功能避免受到错误或者恶意行为的破坏。在第(4)步中,OAuth 2 提供了终端用户做授权决策的机会,终端用户可以同意授予对应用程序请求的一个或多个作用域的访问权限,或者拒绝该请求。最终授权服务器颁发的访问令牌只绑定了用户授权决策后的作用域集合。这是OAuth 2设计授权作用域的直接原因。

如何定义和设计Scope

如上面我们截图的谷歌日历权限范围定义,权限范围的设计包括权限范围值和对应的描述,其中权限范围值用于客户端初始化请求,权限范围描述用于在终端用户做授权决策的阶段向用户展示客户端正在申请的权限范围。权限范围描述必须直观和清晰,这样可以使用户快速了解他即将向客户端应用程序授予的权限范围以便他能快速正确地做出授权决策。
 
那么对于权限范围值的设计呢?OAuth 2 并没有为作用域定义任何特定的值(但是 OIDC 定义了四个标准的 scope值),因为它高度依赖于服务的内部架构和需求,这就需要授权服务供应商仔细斟酌,粒度过粗或过细的 scope 设计都会对使用者造成困惑。同时权限范围值的命名也具有挑战性,名字是一个人的符号,我们不能随意使用一个没有意义的字符串,如果这样做这同样会使客户端应用程序开发者陷入迷茫。虽然 scope 值的设计并没有金科玉律,但总有一些好的实践原则可以遵循,下面我们以谷歌日历为例做出讨论。
 
如下图所示,谷歌日历的目的是开放这些能力。

设计细粒度的作用域

一种最简单直接的做法是为每一个操作定义一个scope值,如下图所示。
这样做的好处显而易见,这提供了足够的自由度——不仅仅对应用程序开发者,也对终端用户。但坏处呢?scope的粒度越小,就越复杂。试想一下,这仅仅是一个简单的示例,如果是一个更复杂的场景呢?终端用户需要花费大量的时间来阅读长长的scope列表,以此判断他们操作的后果。

设计粗粒度的 scope

另一种设计 scope 的思路则更为粗暴和简单。很明显上面长长的列表可以分为两类,那么我们可以直接按照分类分成两个 scope。
  1. 查看,编辑,共享和永久删除您可以使用 Google 日历访问的所有日历。
  2. 查看、编辑、删除、新增您所有日历上的事件。
这在有些情况下是可行的。如果按照分类的角度粗暴地划分scope不能满足业务,另外一种设计 scope 的思路是基于消费方使用 scope 的目标。例如对于谷歌日历来说,它提供API的目标可能是管理谷歌日历、管理谷歌日历事件、共享谷歌日历事件等,因此这也是一种设计粗粒度scope的思路。

根据业务设计混合模式的作用域

不管是细粒度还是粗粒度都各有利弊,另外一种模式是粗粒度和细粒度混合设计,这为应用程序开发者提供了选择的权利。如下图所示,谷歌日历在 scope 的设计上使用了混合的模式,scope 之间可能存在权限重叠的情况。

令牌类型

OAuth 2 中有两种类型的令牌,即访问令牌(access token)和刷新令牌(refresh token)。访问令牌是一种短期令牌,是客户端应用程序在请求受保护的资源时使用的令牌,它是终端用户对客户端成功授权的产物。而刷新令牌是用于在访问令牌过期时更新访问令牌的令牌,它是一种长期令牌。客户端应用程序必须安全地存储访问令牌和刷新令牌。短期是指一小时或更短,但实际时间可能因授权服务器的策略而异。一到五分钟是访问令牌生命周期的常见长度。我们在授权服务器实现的章节会讨论如何自定义令牌的周期。

访问令牌(Access Token)

在上文我们已经讨论了访问令牌,它代表了用户对客户端授权的受保护资源的结果集,并且它是有时限的。访问令牌的这两个属性分别称为访问范围和访问持续时间。访问令牌的另外一个重要属性是令牌类型,在 OAuth 2 主要定义和使用一种类型的令牌——那就是 barer 类型的令牌,barer令牌被定义在 RFC 6750 规范(https://tools.ietf.org/html/rfc6750)中,这也是接下来我们要讨论的主要内容。然而 barer 类型的令牌并非一种完美无瑕的选择,因此社区正在努力开发和完善另外两种类型的令牌,那就是 MAC (Message Authentication Code )令牌和 DPoP(Demonstration of Proof of Possession)令牌。MAC 令牌主要用在非 HTTPS 的环境,我们在此不过多讨论,如果你对这个议题感兴趣,你可以阅读OAuth 2.0 Message Authentication Code (MAC) Tokens文档获取更多的信息。相对于 barer 令牌,MAC令牌试图解决令牌签名的问题。MAC 令牌就像信用卡,每当你使用信用卡时,你必须用你的签名授权付款。如果有人偷了你的卡,小偷就不能使用它,除非他们知道如何像你一样签名。这是 MAC 令牌的主要优势。DPoP 令牌是另一种有益的尝试,和 MAC 令牌一样,它同样是为了解决令牌有效持有的问题,但是它主要使用 JWT 令牌来解决该问题。DPoP 令牌也并非我们讨论的主题,如果你对这个议题感兴趣,你可以阅读OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)文档获取更多的信息。

barer 令牌

barer 令牌就像是人民币一样,任何持有人民币的人都可以使用它去交换“资源”,在使用人民币时收银员并不会要求你举证你是纸币的合法拥有者或纸币的来源。这也是OAuth barer令牌被人所诟病的地方,任何持有 barer 令牌的对象都可以向资源所有者索取资源。因此 barer 令牌必须始终在 HTTPS 上使用,以避免在传输过程中的丢失。barer 令牌使用的另外一个关键点在于,barer 令牌只对资源服务器和授权服务器有意义,这有这两者才知道如何解析和使用 barer 令牌。它对客户端来说没有任何意义,就是一个随机的字符串。客户端应用程序不应该也不能解析barer 令牌的内容或含义。
 
barer 令牌有三种使用方式。第一种是在 HTTP Authorization 请求头中携带访问令牌。
 GET /resource HTTP/1.1
 
 Host: example.com
 Authorization: Bearer mF_9.B5f-4.1JqM
另一种使用 barer 令牌的方式是在查询参数中携带该令牌。这种方式主要用于由 JavaScript 开发的客户端应用程序。注意的是当使用查询参数携带令牌时,参数名必须是 access_token。
GET /resource?
access_token=mF_9.B5f-4.1JqM

Host: example.com HTTP/1.1
第三种方式是将 barer 令牌作为表单编码的主体参数发送。
POST /resource HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM
但是 OAuth 2.1 中提出的最佳安全实践中使用查询参数携带令牌的方式已经不再推荐了,这是因为查询字符串和URL中的任何字符串都不是私有的,在页面上执行的 JavaScript 可以访问它,它们可以被记录在服务器日志文件、缓存或浏览器的历史记录中。

Opaque Token VS JWT Token

在 OAuth 2 中,barer 令牌有两种表现形式,使用 Opaque Token,即不透明令牌是主要的方式,这种形式的令牌就是一个随机的无意义的字符串,它所有有意义的敏感信息,例如令牌对应的终端用户、客户端信息、令牌过期时间、令牌的生成时间、绑定的 scope 信息等都存储在生成它的授权服务器数据库中,而令牌就是一个查询这些信息的索引。如下图所示,当使用这种形式的访问令牌时,当客户端应用程序使用访问令牌向资源服务器“交换”终端用户的资源时,资源服务器不得不每次向授权服务器发起一次 HTTP 调用询问当前令牌的有效信息,比如我上面提到的令牌的生成时间、绑定的 scope 等信息。
barer 令牌的另外一种形式是自包含的令牌,这种形式的令牌自身已经裹挟了很多有用的信息,比如令牌的生成时间、绑定的 scope 等信息,IETF 工作组定义了这种令牌的标准,这就是在第(12)小节我向你描述的 JWT 令牌。如下图所示,当资源服务器收到这种形式的令牌时它不必每次都向授权服务器发起 HTTP 调用问询令牌背后的信息。
JWT 令牌看起来是一种更好的选择,但使用 JWT 令牌并非银弹,相反目前市场上大多数的服务供应商都更愿意选择不透明令牌。它们各有利弊。显而易见,相比于 JWT 令牌,不透明令牌是一种更加简单、安全和容易构建的令牌,不透明令牌背后的所有信息都需要被持久化到数据库中,而 JWT 令牌里裹挟了很多重要和敏感的信息,任何拿到JWT令牌的人都可以看到它包含的重要信息(但这也不是绝对的,如果你使用 JWT 的一种实现形式——JWE令牌,这种令牌可以保证这些信息是被加密且安全的,但JWE并不是一种被广泛使用的令牌)。如上图中的JWT令牌,如果你复制它的内容到https://jwt.io/这个网站,你可以看到这个令牌裹挟的有效信息。
不透明令牌另外一个显著的优势在于非常容易被注销,而 JWT 的一个显著劣势在于它覆水难收,一旦颁发就很难再被注销(如果你对这些不太了解也没关系,我在 JWT 的章节会向你详细介绍这些细节和解决策略)。另外一方面,如果资源服务器和授权服务器是独立部署的,那么每当客户端应用程序使用访问令牌调用资源服务器的资源API,资源服务器不得不向授权服务器发起一次 HTTP 调用问询当前访问令牌的有效信息,包括访问令牌绑定的scope、令牌的过期时间等。这是不透明令牌的劣势,但却也是 JWT 令牌的优势,你在构建授权服务器的时候需要权衡这些利弊,做出合适的抉择。

刷新令牌

相对于访问令牌,刷新令牌是一种“长期”令牌,它主要用于当访问令牌过期时,客户端向授权服务器交换一个新的访问令牌。设计刷新令牌的理由有三个,一是避免频繁地向用户申请授权。想象一下,每次获取访问令牌都需要用户来授权,这给用户造成了不好的用户体验。而刷新令牌可以减少用户授权的频率。另外一个理由是刷新令牌提供了用户在不在现场的情况下获取用户资源的机会。试想一个场景,如果你的应用有一个定时任务,每个星期五都拉取用户的新浪微博的微博数据,并向他发送一封微博数据报告的邮件。用户第一次使用这个功能时需要授权然后应用获得了一个访问令牌,此后的每一周,用户并不在现场,你没有引导和获取用户授权的机会,但是刷新令牌提供了这样的功能——使用刷新令牌获取一个新的访问令牌。提供刷新令牌的第三个理由是安全,访问令牌频繁地被传输和使用,这增大了访问令牌失窃的风险。授权服务方提供一个较短时间的访问令牌,而提供一个较长时间的刷新令牌。这可以最大限度地降低访问令牌失窃的风险。与访问令牌一样,刷新令牌同样也应该被安全地存储和使用。我们将在第(10)小节讨论如何使用刷新令牌。

总结

  • 访问令牌是 OAuth 2.0 工作的基石,使用一个有时限的绑定了若干作用域的令牌来代替共享用户凭证信息这种反模式,这是O Auth 2.0工作的核心原理。
  • 作用域(scope)是另外一个重要的概念,授权服务器颁发的一个访问令牌总是绑定一个或者一组作用域,而资源服务器需要提取访问令牌背后的作用域,
  • barer令牌是当前 OAuth 2.0 访问令牌的主要形式,这种形式的令牌本身有一些被人诟病的缺陷,那就是“不问出处”,任何持有令牌的对象都可以使用令牌来交换资源。因此使用TLS这样的安全传输层机制来传递访问令牌是重要的安全实践原则。除了barer令牌之外,社区正在积极进行一些有益的尝试,包括MAC令牌和DPoP令牌,但这两种令牌正处于试验阶段没有被广泛应用,因此对于这两种类型的令牌仅仅作为了解即可。
  • 使用一个较短生命周期的访问令牌是保证令牌安全的重要实践原则,但是它会牺牲和影响用户体验,而刷新令牌是对用户体验和访问令牌安全性权衡的结果。关于刷新令牌在第(11)小节我还会对它进行更详细地描述。
  • 如何定义和设计scope是授权服务供应商在开发阶段就应该考虑的问题,我在这一小节简单描述了定义scope的方法,如果你正好在设计一个开放平台,参考各个开放平台提供的scope定义文档来设计你的scope不失为一种好的方式。下面是一些耳熟能详的开放平台定义scope的文档,它们提供了一些良好的实践,希望你能从这些文档里收获一些灵感(我只提供了一些较为主流的开放平台提供的开放文档,如果你发现更多提供了关于scope良好定义的开放平台,欢迎你在评论区补充)。
    • 国内开放平台
      1. 新浪微博开放平台:https://open.weibo.com/wiki/%E5%BE%AE%E5%8D%9AAPI
      2. 淘宝开放平台:https://open.taobao.com/api.htm?docId=46
    • 国外主流的OAuth 2.0开放平台
      1. Slack:https://api.slack.com/legacy/oauth-scopes
      2. Github:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
      3. Google:https://developers.google.com/identity/protocols/googlescopes
这一小节向你介绍了关于OAuth 2.0两个基础但重要的概念,至此为止我们已经了解了关于OAuth 2.0的所有基础概念。接下来我将正式向你介绍OAuth 2.0定义的一种重要的授权模式——授权码模式(Authorization code flow)。这是最为推荐和安全的模式。
 
posted @ 2023-05-07 11:24  小米粥|  阅读(109)  评论(0)    收藏  举报