1.什么是 OAuth 2.0,以及你为什么需要关注?¶
本章将介绍
- OAuth 2.0 是什么
- 没有 OAuth 时,开发者通常怎么做
- OAuth 的工作原理
- OAuth 2.0 不是什么
如果你现在是一名做 Web 的软件开发者,大概率听说过 OAuth。它是一种安全协议,用来保护全球范围内大量(而且还在不断增长)的 Web API——从 Facebook、Google 这样的超大规模服务商,到初创公司以及各种规模企业内部那些一次性的、小型 API。OAuth 用于让不同网站之间建立连接,也支撑着原生应用和移动应用连接云服务。越来越多不同领域的标准协议也把它作为安全层,从医疗健康到身份认证,从能源到社交网络。毫无疑问,OAuth 已经成为当今 Web 上最主流的安全方案;它的广泛普及,也让希望为应用加固安全的开发者站在了同一起跑线上。
但它到底是什么?它是怎么工作的?我们为什么需要它?
什么是 OAuth 2.0?¶
OAuth 2.0 是一种委托(delegation)协议
,用来让资源的控制者授权某个软件应用在不冒充自己的前提下,代表自己访问该资源。应用会向资源所有者请求授权,并获得可用于访问资源的令牌(token)。整个过程中,应用无需伪装成资源控制者,因为令牌明确代表的是被委托的访问权。从很多角度看,你可以把
OAuth 的令牌理解为 Web 世界的“代客钥匙(valet
key)”。并不是所有汽车都有代客钥匙,但对于配备代客钥匙的汽车来说,它比直接交出常规钥匙更安全。汽车的代客钥匙允许车主在不交出车主钥匙(即完全控制权)的情况下,把有限的访问权限交给代客泊车人员。简单的代客钥匙只允许对方使用点火和车门,但不能打开后备箱或手套箱。更复杂的代客钥匙还能限制车辆最高时速,甚至在车辆离开起始点超过设定距离时自动熄火,并向车主发送告警。同样地,OAuth
令牌也可以将客户端的访问权限限定在资源所有者所委托的操作范围内。
例如,假设你同时使用一个云端照片存储服务和一个照片冲印服务,并且希望把存储服务里保存的照片拿去冲印。幸运的是,你的云冲印服务可以通过 API 与云存储服务通信。这当然很好,但问题在于这两个服务属于不同公司运营,这意味着你在存储服务的账号与冲印服务的账号彼此并不关联。我们可以用 OAuth 来解决这个问题:让你在不同服务之间委托对照片的访问权限,而无需把密码交给照片冲印服务。
尽管 OAuth 基本不关心它所保护的资源具体是什么类型,但它与当下的 RESTful Web 服务非常契合,并且同时适用于 Web 客户端与原生客户端应用。它既能用于小型的单用户应用,也能扩展到拥有数百万用户的互联网 API。它既适用于狂野无序的 Web 环境——OAuth 正是在那里成长起来,并被用于保护各种面向用户的 API——也同样适用于企业内部可控且受监控的边界之内,在那里它被用来管理新一代内部业务 API 与系统的访问权限。
还不止如此:如果你在过去五年里使用过移动端或 Web 技术,那么你很可能已经用过 OAuth,把自己的授权委托给某个应用。事实上,只要你见过类似图 1.1 所示的网页,那么不管你是否意识到,你都已经使用过 OAuth。
在很多情况下,OAuth 协议的使用对用户来说几乎是完全“无感”的,比如 Steam 和 Spotify 的桌面客户端。除非终端用户刻意去找 OAuth 交互的那些典型特征,否则他们根本不会意识到它正在被使用。1 这其实是件好事,因为一个优秀的安全系统在一切正常运转时,应该尽可能“隐形”。
我们知道 OAuth 是一种安全协议,但它究竟做什么?既然你手里拿着一本号称讲 OAuth 2.0 的书,这确实是个合理的问题。根据定义它的规范:2
OAuth 2.0 授权框架使第三方应用能够获得对某个 HTTP 服务的有限访问权限:既可以通过在资源所有者与该 HTTP 服务之间协调并完成授权交互,代表资源所有者获取访问权限;也可以允许第三方应用以自身名义获取访问权限。
让我们把这个问题拆开来说:作为一种授权框架,OAuth 的核心就是让系统中的一个组件把“访问权限”授予另一个组件。具体而言,在 OAuth 的语境里,客户端应用希望代表资源所有者(通常是最终用户)去访问受保护资源。到目前为止,我们涉及的组件包括:
资源所有者(resource owner):拥有对某个 API 的访问权,并且可以把对该 API 的访问权限委托出去。资源所有者通常是一个人,一般默认可以使用 Web 浏览器。因此,本书的图示会把这一方画成坐在浏览器前的一个人。受保护资源(protected resource):资源所有者可以访问的那个组件。它可以有很多种形态,但大多数情况下是某种 Web API。尽管“资源(resource)”这个名字听起来像是要下载的东西,但这些 API 同样可以支持读取、写入以及其他各种操作。本书的图示会把受保护资源画成一排带锁图标的服务器机架。客户端(client):代表资源所有者去访问受保护资源的软件。如果你是 Web 开发者,“客户端”这个词可能会让你想到浏览器,但这里并不是这个意思;如果你是企业应用开发者,你可能会把“client”理解为付费购买你服务的客户,但我们也不是在说这个。在 OAuth 中,客户端指的是任何消费受保护资源所提供 API 的软件。本书里只要看到“客户端”,几乎可以肯定说的都是这种 OAuth 语境下的定义。本书的图示把客户端画成带齿轮的电脑屏幕。这在一定程度上也是考虑到客户端应用形态非常多样——我们会在「真实世界中的 OAuth 2.0」看到——所以没有哪个图标能通用于所有情况。
「OAuth 之舞」我们会在详细讲解“The OAuth Dance”(OAuth 授权流程)时更深入地介绍这些概念。不过现在,你需要先明确:这一整套机制只有一个目标——让客户端能够为资源所有者访问受保护资源(见图 1.2)。
在这个打印的例子里,假设你已经把度假照片上传到了照片存储网站,现在想把它们冲印出来。存储网站提供的 API 就是资源,打印服务则是该 API 的客户端。你作为资源所有者,需要能够把自己权限中的一部分委托给打印服务,让它可以读取你的照片。你大概率不希望打印服务能查看你所有的照片,也不希望它能删除照片,或者自行上传新的照片。归根结底,你真正关心的是把特定的照片打印出来;而且如果你和大多数用户一样,你并不会去思考为了完成这件事,所使用的那些系统背后的安全架构。
好在你正在读这本书,因此你很可能并不属于“大多数用户”——你确实在意安全架构。在下一节里,我们会先看看在没有 OAuth 的情况下,这个问题可以如何以一种并不完美的方式被解决;然后再看看 OAuth 如何用更好的方式解决它。
糟糕的旧时代:凭据共享(以及凭据被盗)¶
想要把多个彼此独立的服务连接起来,这个问题几乎一点也不新。完全可以说,从世界上出现不止一个可联网的服务那一刻起,这个问题就已经存在了。
一种做法在企业领域曾相当常见:复制用户的凭据,并在另一个服务上“重放”这些凭据(见图 1.3)。在这种情况下,照片打印机默认用户在打印机上使用的凭据与其在存储站点上使用的是同一套。用户登录打印机后,打印机会拿着用户的用户名和密码去存储站点再次登录,从而获取用户在那边的账号访问权限——本质上就是冒充用户。
在这种情况下,用户需要使用某种凭据在客户端进行身份验证,通常是客户端与受保护资源双方都认可、并由中心化机制统一管理的凭据。随后,客户端会拿着该凭据(例如用户名和密码,或域会话 Cookie),再把它“重放”给受保护资源,冒充用户完成登录。受保护资源会像用户亲自完成认证一样处理请求,从而确实建立起客户端与受保护资源之间所需的连接。
这种做法要求用户在客户端应用和受保护资源上使用同一套凭据,因此这种“窃取凭据”的手段只在单一安全域内有效。比如,当同一家公司同时控制客户端、授权服务器以及受保护资源,并且这些系统都运行在相同的策略体系和网络管控之下时,就可能发生这种情况。如果打印服务与存储服务由同一家公司提供,那么这种手法可能就行得通,因为用户在两项服务上会使用相同的账号凭据。
这种手法也会把用户的密码暴露给客户端应用。尽管在同一安全域内、共用一套凭据的场景下,这种情况本就很可能已经存在。问题在于,客户端是在冒充用户,而受保护资源无法区分真正的资源所有者与冒充者客户端,因为二者使用的是同一个用户名和密码,方式也完全一致。
但如果这两项服务处在不同的安全域中呢?这在我们的照片打印示例里很可能才是常态。此时,我们就无法再复制用户提供给我们用于登录本应用的密码,因为它在远程站点上根本不通用。面对这一难题,这些“准”凭据窃贼就会采用一种古老的偷窃办法:直接向用户索要(见图 1.4)。
如果打印服务想获取用户的照片,它可以提示用户输入其在照片存储网站上的用户名和密码。和之前一样,打印机把这些凭据转发到受保护资源上进行重放,从而冒充用户。在这种场景下,用户用来登录客户端的凭据可以不同于用来访问受保护资源的凭据。但客户端会通过要求用户为受保护资源单独提供用户名和密码来绕过这一点。事实上,很多用户都会这么做,尤其是在被承诺提供某种基于受保护资源的实用服务时。因此,直到今天,这仍然是移动应用通过用户账号访问后端服务最常见的方式之一:移动应用提示用户输入其凭据,然后通过网络把这些凭据直接重放到后端 API 上。为了持续访问 API,客户端应用会保存用户的凭据,以便需要时随时重放。这是一种极其危险的做法,因为任何正在使用的客户端一旦被攻破,就会导致该用户在所有系统上的账号被彻底攻陷。
这种做法仍然只在非常有限的情况下才行得通:客户端必须能够直接拿到用户的凭据,而且这些凭据必须可以在用户不在场的情况下,对外部服务进行重放验证。这会排除掉大量用户登录方式,包括几乎所有联邦登录、许多多因素认证方案,以及大多数安全级别更高的登录系统。
轻量级目录访问协议(LDAP)认证
有意思的是,这种模式正是 LDAP 这类“密码保管库式”认证技术的工作方式。使用 LDAP 做认证时,客户端应用会直接向用户收集凭据,然后把这些凭据转发(重放)给 LDAP 服务器,用来确认它们是否有效。在这次交互过程中,客户端系统必须能够拿到用户的明文密码;否则,它就无法与 LDAP 服务器完成验证。从某种非常现实的意义上说,这种方法相当于对用户实施了一种“中间人攻击”,只不过通常动机是善意的。
即便在它确实可用的场景里,这种方式也会把用户的主凭据暴露给可能并不可信的应用——也就是客户端。为了后续还能继续以用户身份行动,客户端必须以可重放的形式保存用户密码(往往是明文,或采用可逆加密机制),以便之后在受保护资源处使用。如果客户端应用一旦被攻破,攻击者获得的就不仅是客户端的控制权,还能进一步访问受保护资源,以及用户在其他任何复用了同一密码的服务。
此外,在这两种做法中,客户端应用都在冒充资源所有者,而受保护资源无法区分:一次调用究竟是资源所有者本人直接发起的,还是经由客户端转发而来。为什么这很糟糕?我们回到打印服务的例子。很多方案在特定限制条件下都能跑通,但请想想:你并不希望打印服务能从存储服务上传或删除照片。你只希望它读取你想打印的那些照片;你也只希望它在你需要打印的这段时间里能读;并且你希望随时都能关闭这种访问权限。
如果打印服务必须冒充你才能访问你的照片,那么存储服务就无法判断发起操作的到底是你还是打印机。如果打印服务在后台偷偷复制了你的密码(即便它承诺不会这么做),它就能随时冒充你,想拿走你的照片就拿走。要关掉这个“作恶”的打印服务,唯一办法是去存储服务修改密码,从而让它手里那份密码副本失效。再结合很多用户会在不同系统之间复用密码这一事实,结果就是又多了一个密码可能被盗、账号可能被相互关联的入口。坦白说,为了解决连接问题,我们反而把事情搞得更糟。
到这里你应该已经明白:重放用户密码是很糟糕的。那如果反过来,我们给打印服务一把“万能钥匙”,让它可以代表任何它想代表的人,访问存储服务上的所有照片呢?另一种常见做法是给客户端发放一个开发者密钥(见图 1.5),客户端用它直接调用受保护资源。
在这种方案中,开发者密钥相当于一把“万能钥匙”,让客户端可以(很可能通过某个 API 参数)冒充它所选择的任何用户。这样做的好处是不会把用户的登录凭据暴露给客户端,但代价是客户端必须持有一种权限极高的凭据。我们的打印服务因此可以在任何时间、以任何用户的身份,随意打印它想打印的任何照片,因为客户端实际上对受保护资源上的数据拥有完全的操控权。这个办法在一定程度上可行,但只适用于受保护资源能够完全了解并信任客户端的场景。像我们照片打印的案例那样跨越两个组织建立这种信任关系,几乎不可能。此外,一旦客户端的凭据被盗,对受保护资源造成的破坏可能是灾难性的:存储服务的所有用户都会受到这次泄露的影响,不管他们是否使用过打印机。
另一种可能的做法是给用户提供一个专门的密码(图 1.6),用于与第三方服务共享。用户自己并不使用这个密码登录,而是把它粘贴到那些他们希望替自己工作的应用中。这听起来就有点像你在本章开头看到的那把“受限使用”的代客钥匙。
这已经开始更接近一个理想的系统了:用户不再需要把真实密码交给客户端;受保护资源也不必在任何时候都隐式地信任客户端会代表所有用户正确行事。不过,这样的系统单独来看,可用性并不高。它要求用户除了维护自己已有的主密码之外,还要生成、分发并管理这些特殊凭据。由于这些凭据需要由用户自行管理,一般来说,客户端程序与凭据本身之间也就不存在明确的关联关系。这就导致很难针对某个特定应用撤销访问权限。
难道就不能做得更好吗?
如果我们能为每一组“客户端 + 用户”的组合分别签发这种受限凭据,并让它在受保护资源处使用,会怎么样?这样一来,我们就可以把有限的权限精确绑定到每一个受限凭据上。再进一步设想:如果有一种基于网络的协议,能够跨越安全边界,以既友好又可扩展到整个互联网的方式,生成并安全分发这些受限凭据呢?那就开始变得有意思了。
授权委托¶
OAuth 正是为此而设计的协议:在 OAuth 中,终端用户将其访问受保护资源的部分权限委托给客户端应用,由客户端代表其执行操作。为实现这一点,OAuth 在系统中引入了另一个组件:授权服务器(见图 1.7)。
授权服务器(AS)受受保护资源信任,负责向客户端签发专用的安全凭据——即 OAuth 访问令牌(access token)。为获取令牌,客户端会先将资源所有者引导至授权服务器,请求资源所有者授权该客户端。资源所有者在授权服务器上完成认证,通常会看到是否同意授权发起请求的客户端的选项。客户端可以申请一部分功能(scope,作用域),而资源所有者还可以在此基础上进一步收窄授权范围。授权许可一旦授予,客户端便可向授权服务器申请访问令牌。该访问令牌可用于在受保护资源处访问 API,访问权限以资源所有者授予的范围为准(见图 1.8)。
在整个流程中,资源所有者的凭据在任何时候都不会暴露给客户端:资源所有者会在授权服务器上完成认证,这一过程与客户端通信所用的任何信息都是相互独立的。客户端也不会持有权限极高的开发者密钥:它无法凭自身能力访问任何内容,必须先获得有效资源所有者的授权,才能访问受保护资源。即便多数 OAuth 客户端都具备向授权服务器证明自身身份的手段,这一点依然成立。
用户通常无需直接看到或处理访问令牌。OAuth 协议不会要求用户手动生成令牌并复制粘贴到客户端,而是将这一过程标准化并简化,使客户端可以更容易地申请令牌,用户也能更便捷地对客户端进行授权。随后,客户端负责管理令牌,用户则负责管理客户端应用。
以上是对 OAuth 协议工作机制的总体概述。实际上,使用 OAuth 获取访问令牌的方式有好几种。我们会在「OAuth 之舞」通过更深入地解析 OAuth 2.0 的“授权码(Authorization Code)”授权类型,来讨论该流程的细节。其他获取访问令牌的方法将在「真实世界中的 OAuth 2.0」介绍。
超越 HTTP Basic 与“共享密码”的反模式¶
上一节列出的许多较为“传统”的方案,其实都是一种密码反模式:用一个共享秘密(密码)直接代表某个主体(用户)。用户把这个秘密密码交给应用后,应用就能据此访问受保护的 API。然而,正如我们所展示的那样,这在现实世界里问题重重:密码可能被窃取或被猜中;同一用户往往会把某个服务的密码原封不动地用到另一个服务上;而为了日后继续访问 API 而存储密码,又会让密码更容易被盗。
那 HTTP API 一开始为什么会变成“用密码保护”的?回顾 HTTP 协议及其安全机制的发展史会很有启发。HTTP 定义了一种机制:浏览器中的用户可以通过一种称为 HTTP Basic Auth 的协议,使用用户名和密码对网页进行认证。它还有一个稍微更安全的版本,叫 HTTP Digest Auth;但就我们这里的讨论而言,两者可视为等价——它们都默认“有用户在场”,并且实际上都要求向 HTTP 服务器出示用户名和密码。此外,由于 HTTP 是无状态协议,通常假定每一次事务都要再次提交这些凭据。
考虑到 HTTP 最初是一个文档访问协议,这一切都说得通。但自早期以来,Web 在覆盖范围和使用广度上都已发生巨变。作为协议,HTTP 并不区分“用户在浏览器中参与的交易”和“没有浏览器中介、由另一段软件直接发起的交易”。这种根本性的灵活性,正是 HTTP 协议取得难以置信的成功与普及的关键。但也正因为如此,当 HTTP 除了面向用户的服务外也开始承载“直连式 API”时,它既有的安全机制就很快被沿用到这一新场景中。这个看似简单的技术选择,促成了长期以来对“每次都提交密码”的误用——无论是 API 还是面向用户的页面都如此。浏览器有 Cookie 和其他会话管理手段可用,而通常用来调用 Web API 的那类 HTTP 客户端却并不具备这些能力。
OAuth 从一开始就是为 API 设计的协议,核心交互发生在浏览器之外。它通常会让最终用户先在浏览器里启动流程——这也正是委派模型灵活性与威力的来源——但最终“拿到令牌”并在受保护资源处使用它的步骤,对用户是不可见的。事实上,OAuth 的一些关键用例发生在用户已不再在客户端现场的情况下,而客户端仍能代表用户执行操作。使用 OAuth,我们就能以一种强大、安全、并且面向当今 API 经济而设计的方式,摆脱 HTTP Basic 协议所隐含的观念与假设。
授权委派:它为什么重要,以及如何使用¶
OAuth 的强大之处,根源在于“委派”这一概念。尽管 OAuth 常被称为授权协议(定义它的 RFC 也确实这么命名),但它本质上是一个委派协议。一般来说,被委派的是用户授权中的一个子集,但 OAuth 本身并不承载或传递具体的授权信息。相反,它提供一种方式,让客户端可以请求用户将部分权限委派给它;用户随后可以批准该请求,客户端则可以基于这一批准结果采取行动。
在我们的打印示例中,照片冲印服务可以问用户:“你的照片是不是存放在这个存储网站上?如果是,我们完全可以直接帮你把它们打印出来。” 接着用户会被带到照片存储服务那里,后者会提示:“这个冲印服务正在请求获取你的部分照片;你是否同意?” 用户就可以据此决定是否允许,进而决定是否将对冲印服务的访问权限委派出去。
这里区分“委派协议”和“授权协议”非常重要,因为 OAuth 令牌所携带的授权对系统的大多数部分而言都是不透明的。只有受保护资源需要理解授权内容;只要它能从令牌及其呈现上下文中获知这些信息(要么直接解析令牌,要么通过某种服务获取相关信息),它就能按需提供 API 服务。
连接在线世界
OAuth 里的许多概念并不新鲜,甚至其具体落地也在很大程度上借鉴了前几代安全系统。然而,OAuth 是为 Web API 世界设计的协议,供客户端软件调用。尤其是 OAuth 2.0 框架,提供了一整套工具,用于在各种用例中将应用与 API 连接起来。正如我们在后续章节将看到的,同一套核心概念与协议可以用于连接浏览器应用、Web 服务、原生与移动应用,甚至(通过一些扩展)还能覆盖物联网中的小型设备。贯穿这一切,OAuth 都依赖于一个在线互联的世界,并使我们能够在这一层之上构建新的能力。
用户驱动的安全与用户选择¶
由于 OAuth 的委派流程包含资源所有者,它带来了一种在许多其他安全模型中都找不到的可能性:关键的安全决策可以由终端用户的选择来驱动。传统上,安全决策属于集中式权威机构的职责范围;这些机构决定谁能使用某项服务、使用哪种客户端软件、以及用于何种目的。OAuth 让这些权威机构可以把一部分决策权交到最终使用软件的用户手中。
OAuth 系统往往遵循 TOFU 原则:首次使用即信任(Trust On First Use)。在 TOFU 模型中,当运行时需要做出某个安全决策,而系统又不存在可据以判断的既有上下文或配置时,就会提示用户。这可能很简单,比如“连接一个新应用?”;不过很多实现也允许用户在这一步进行更细粒度的控制。无论具体交互如何,只要用户具备相应权限,就可以做出安全决策;系统还会提供选项,让它记住该决定以便下次使用。换句话说,当第一次遇到某个授权上下文时,系统可以被指示在后续处理中信任用户当时的决定:首次使用即信任。
我必须“吃 TOFU”吗?
OAuth 的实现并不强制要求采用 TOFU 来管理安全决策,但这两者同时出现的情况尤其常见。为什么?TOFU 在“让终端用户在上下文中做安全决策的灵活性”和“不断让用户做决定带来的疲劳”之间取得了良好平衡。如果没有 TOFU 的“信任(Trust)”部分,用户就无法对这些委派如何发生拥有发言权;如果没有“首次使用(On First Use)”部分,用户很快就会对无穷无尽的访问请求麻木。这类安全系统疲劳会催生各种绕过手段,而这些手段通常比安全系统试图纠正的做法更不安全。
这种方法也把用户的选择表述为“功能”,而不是“安全”:“你是否希望这个客户端去做它所请求的事情?” 这与更传统的安全模型形成了重要差异:在传统模型中,决策者往往需要提前划定哪些行为不被允许。对普通用户而言,这类安全决策常常令人不堪重负;而且无论如何,用户更关心自己要完成什么,而不是要阻止什么。
当然,这并不是说 TOFU 必须用于所有事务或所有决策。在实践中,三层列表机制为安全架构师提供了强大的灵活性(见图 1.9)。
白名单用于确定已知可信、可安全放行的应用;黑名单用于标记已知恶意的应用或其他不良行为主体。这类决策很容易从终端用户手中剥离出来,由系统策略预先(a priori)统一裁定。在传统安全模型中,讨论通常到此为止:默认情况下,凡是不在白名单上的内容都会自动归入黑名单。然而,引入 TOFU 方法后,我们可以在两者之间加入一个“灰名单”——一片未知区域,在这里,基于用户的运行时信任决策可以优先生效。这些决策可以被记录并接受审计,同时也能通过策略将被攻破的风险降到最低。通过提供灰名单能力,系统可以在不牺牲安全性的前提下,大幅拓展其可用方式。
OAuth 2.0:优点、缺点与“坑”¶
OAuth 2.0 非常擅长捕捉用户的授权委托决策,并将其在网络中表达出来。它允许多个不同的参与方共同参与安全决策过程,尤其是在运行时由最终用户直接参与。它是一套由许多不同组件构成的协议,但在很多方面,它比替代方案更简单也更安全。
OAuth 2.0 设计中的一个关键假设是:在现实世界中,客户端的数量将始终比授权服务器或受保护资源服务器多出好几个数量级(见图 1.10)。这一点很合理,因为一个授权服务器就可以轻松保护多个资源服务器,而且往往会有各种类型的客户端想要消费同一个 API。一个授权服务器甚至可以同时面对多个不同类别的客户端,并对它们给予不同级别的信任,不过更深入的内容我们会在「动态客户端注册」展开。基于这一架构决策,协议在可能的情况下会把复杂性从客户端转移到服务器端。这对客户端开发者非常友好,因为客户端会成为整个系统中最简单的一环。客户端开发者不再需要像以往的安全协议那样处理签名规范化,或者解析复杂的安全策略文档,也不必再担心处理敏感的用户凭据。OAuth 令牌提供了一种机制:复杂度只比密码略高,但在正确使用时安全性显著更强。
不利的一面是,授权服务器和受保护资源现在需要承担更多的复杂性与安全责任。客户端只需要保护好自身的客户端凭据以及用户的令牌;即便某个客户端被攻破,影响也很糟糕,但损害范围通常仅限于该客户端的用户。并且,客户端被攻破也不会暴露资源所有者的凭据,因为客户端从一开始就不会接触到这些信息。相较之下,授权服务器必须管理并保护系统中所有客户端与所有用户的凭据和令牌。尽管这会让它更容易成为攻击目标,但要把一个授权服务器做到高度安全,远比让一千个由不同开发者编写的客户端都达到同样的安全水准要容易得多。
OAuth 2.0 的可扩展性与模块化是其最大的优势之一,因为它让协议可以适用于各种不同的环境。然而,正是这种灵活性也会导致不同实现之间出现基础性的兼容问题。OAuth 将许多部分设为可选项,这会让试图在两个系统之间实现它的开发者感到困惑。
更糟的是,OAuth 提供的一些可选机制如果在错误的语境下使用,或未被正确约束与执行,就可能导致不安全的实现。这类漏洞在《OAuth 威胁模型文档》3以及本书的漏洞章节(第 7、8、9、10 章)中有详细讨论。简而言之,一个系统实现了 OAuth——甚至严格按规范正确实现——并不意味着它在实际运行中就是安全的。
归根结底,OAuth 2.0 是个不错的协议,但离“完美”还差得远。就像技术领域的所有事物一样,它终有一天会被替代。不过截至本书写作时,仍没有出现真正有力的竞争者。同样很有可能的是,OAuth 2.0 的继任者最终会以 OAuth 2.0 自身的某种配置文件(profile)或扩展(extension)的形式出现。
OAuth 2.0 不是什么¶
OAuth 被广泛用于各种类型的 API 和应用,以前所未有的方式把在线世界连接起来。尽管它正接近无处不在,但 OAuth 依然有很多“不是”的地方;在理解协议本身时,弄清这些边界非常重要。
由于 OAuth 被定义为一个框架,历史上关于什么才“算”OAuth、什么不算,一直存在一些混淆。就本文的讨论而言,更确切地说,就本书而言,我们所说的 OAuth 指的是由 OAuth 核心规范4 定义的协议,它详细说明了获取访问令牌(access token)的几种方式。我们也将随附规范5 中定义的 Bearer Token 一并纳入讨论,该规范规定了如何使用这种特定类型的令牌。这两件事——如何获取令牌,以及如何使用令牌——构成了 OAuth 的核心。正如本节将展示的那样,在更广泛的 OAuth 生态中还有不少其他技术会与 OAuth 核心协同工作,从而提供超出 OAuth 自身能力范围的更强功能。我们认为,这样的生态恰恰是协议健康的体现,不应与协议本身混为一谈。
OAuth 并不是在 HTTP 协议之外定义的。由于使用 Bearer Token 的 OAuth 2.0 不提供消息签名机制,它本就不适合在 HTTPS(基于 TLS 的 HTTP)之外使用。敏感的密钥和信息会在网络中传输,而 OAuth 需要像 TLS 这样的传输层机制来保护这些秘密。当前已经有标准用于在受简单认证与安全层(SASL)保护的协议上承载 OAuth Token,6 也有新的工作尝试在受限应用协议(CoAP)上定义 OAuth,7 未来的进一步工作还可能让 OAuth 流程中的某些部分能够在非 TLS 链路上使用(例如「bearer 令牌以外的选择」讨论的一些方案)。但即便在这些情况下,也必须清晰地把 HTTPS 事务映射到其他协议与系统中。
OAuth 并不是一种认证协议,尽管你可以用它来构建认证体系。正如我们将在「使用 OAuth 2.0 进行用户认证」更深入讨论的那样,单独一次 OAuth 交互并不能告诉你用户是谁,甚至不能证明用户是否在场。拿我们“照片打印”的例子来说:照片打印服务不需要知道用户是谁,它只需要知道“有人”同意它去下载一些照片。从本质上讲,OAuth 更像是一种“配方里的原料”,可以在更大的体系中被用来实现其他能力。此外,OAuth 在多个环节会用到认证,尤其是资源所有者和客户端软件向授权服务器进行认证。但这种嵌入式认证并不意味着 OAuth 本身就是认证协议。
OAuth 并没有定义用户与用户之间的委托机制,尽管它的核心确实是在“用户把权限委托给软件”。OAuth 假设资源所有者就是在控制客户端的那个人。要让资源所有者把权限授权给另一个用户,仅靠 OAuth 还不够。这类委托场景并不少见,而用户管理访问(User Managed Access,UMA)协议(「基于 OAuth 2.0 的协议与配置文件」讨论)就是在 OAuth 的基础上构建了一个支持用户对用户委托的系统。
OAuth 也不定义授权处理机制。OAuth 提供的是一种方式,用来传递“授权委托已经发生”这一事实,但它并不规定这份授权的具体内容。相反,需要由服务 API 的定义来结合 OAuth 的组件(例如 scope 与 token)来明确:某个 token 允许执行哪些操作。
OAuth 不规定 token 的格式。事实上,OAuth 协议明确指出:对客户端应用来说,token 的内容必须是完全不透明的(opaque)。这与此前一些安全协议(如 WS-*、安全断言标记语言 SAML、或 Kerberos)不同,在那些协议里,客户端应用需要能够解析并处理 token。不过,token 仍然必须能被签发它的授权服务器以及接收它的受保护资源所理解。对这一层互操作性的需求,催生了 JSON Web Token(JWT)格式与 Token Introspection(令牌自省)协议,并将在「OAuth 令牌」讨论。token 对客户端仍然是不透明的,但其他参与方现在可以理解其格式。
OAuth 2.0 与 OAuth 1.0 不同,它不定义任何密码学方法。OAuth 2.0 并不试图制定一套专属于自己的加密机制,而是被设计为能够复用更通用的密码学机制——这些机制在 OAuth 之外也同样适用。这种有意的“留白”推动了 JSON Object Signing and Encryption(JOSE)规范套件的发展,它提供了通用的密码学能力,可与 OAuth 一起使用,甚至也能在 OAuth 之外独立使用。我们会在「OAuth 令牌」看到更多 JOSE 规范,并在「bearer 令牌以外的选择」把它们应用到基于 OAuth 持有证明(Proof of Possession,PoP)token 的消息级密码协议中。
OAuth 2.0 也并不是单一协议。正如前面所述,该规范被拆分为多个定义与流程(flow),每一种都有各自的适用场景。OAuth 2.0 的核心规范曾被相当贴切地称为“安全协议生成器”,因为它可以用来为许多不同用例设计安全架构。正如上一节讨论的那样,这些系统彼此之间并不一定兼容。
不同 OAuth 流程之间的代码复用
尽管形态各异,OAuth 的不同用法仍然允许在差异很大的应用之间实现大量代码复用;而对 OAuth 协议的谨慎运用,也能为未来在意料之外方向上的扩展与灵活性留足空间。比如,假设有两个后端系统需要安全通信,但并不需要关联某个具体终端用户,可能是在做批量数据传输。如果客户端和资源都处在同一个可信安全域里,传统的开发者 API Key 也能处理这种场景。然而,如果系统改用 OAuth 的客户端凭证授权(client credentials grant,「真实世界中的 OAuth 2.0」讨论),就能限制传输中 token 的生命周期与访问权限;同时,开发者还可以在客户端与受保护资源两端直接复用现成的 OAuth 库与框架,而不必完全自研一套方案。由于受保护资源本来就已经具备处理 OAuth 访问 token 保护请求的能力,将来当它希望以“按用户委托”的方式开放数据时,也可以很容易同时支持两种访问模式。例如,通过为批量传输和用户级数据分别设置不同的 scope,资源端只需极少的代码改动,就能轻松区分这两类调用。
OAuth 并不试图成为一个包打天下、解决安全系统所有方面的“巨无霸协议”,而是专注把一件事做好,同时为其他组件在更合适的地方发挥作用留出空间。尽管 OAuth 有很多“不是什么”,但它确实提供了一个坚实的基础,可以让其他更聚焦的工具在其之上构建,从而形成更完整的安全架构设计。
小结¶
OAuth 是一个被广泛采用的安全标准,它以一种对 Web API 更友好的方式,实现对受保护资源的安全访问。
- OAuth 关注的是如何获取 token,以及如何使用 token。
- OAuth 是一种委托协议,用于在不同系统之间进行授权。
- OAuth 用一种更安全、也更易用的委托协议,取代了共享密码这种反模式。
- OAuth 专注于解决一小组问题并把它们解决好,因此非常适合作为更大型安全系统中的组件。
准备好深入了解 OAuth 在“线上交互”层面到底是如何做到这一切的吗?继续往下读,看看 “OAuth 之舞(The OAuth Dance)” 的细节。