本文由 VIRAG MODY 发表,经原作者授权由 InfoQ 中文站翻译并分享。
现代社会中,人们可能会在几十个应用上都建立了账户。不论是社交媒体应用、流媒体应用还是办公资源,我们每个人都必须管理很多包含个人信息的账户。随着时间流逝,这些孤立的应用程序之间的联系越来越紧密。Twitter 允许新闻网站直接发推文;Discord 在 Facebook 上搜索推荐的朋友;Jira 使用 Github 个人资料创建用户帐户。这种允许应用程序代表你与其他应用交流的趋势称为“委托访问”。这已经成为我们每个人在网络生活中必不可少的一部分。
但是,由于这些应用程序保护数据的方式,开发人员遇到一个挑战:我们如何委派访问权限?几乎每个应用程序都由基本的用户名/密码架构来保护。要允许应用程序 A 访问在应用程序 B 上的所需数据,则需要代表应用程序 A 不断登录应用程序 B——这真是个笨办法!另一个解决方法是将用于登录应用程序 B 的用户名和密码证书告知应用程序 A,这个方法也存在如下一些问题:
应用程序 A 将证书以明文形式存储
应用程序 A 被赋予无限制访问应用程序 B 的权限
用户无法轻易撤回对应用程序 A 的授权
已被攻击成功的应用程序 A 使得应用程序 B 也面临泄漏风险
OAuth 就是为了解决这种左右为难的困境而生:如何在不使最终用户的数据面临风险的情况下提供委托访问。
术语表
它有助于理解协议中的常见术语。注意:在本文中,OAuth 指 OAuth 2.0,是自 2012 年以来的最新版本。
资源所有者(Resource Owner):可以授权对受保护资源的访问的实体(也就是我们)。
资源服务端(Resource Server):存储受保护的资源并处理访问请求的服务端。
客户端(Client):想要访问资源服务端、并代表资源所有者执行操作的应用程序
授权服务端(Authorization Server):知道资源服务端,并且能够授权客户端访问资源服务端的授权服务端。
OAuth 协议工作流
归根结底,OAuth 的存在是为了向第三方应用程序提供对安全资源的有限访问权限,同时避免暴露用户数据。
想想特斯拉的“代泊模式”需要你为其提供代泊钥匙卡。这个钥匙卡告知了车辆它将以有限的功能运行:限制最高时速和加速度并关闭行李箱。此技术在概念上与 OAuth 的运作方式是相同的。特斯拉车主可以在不提供车辆主钥匙的情况下,让代泊司机以有限的权限驾驶他/她的特斯拉。
OAuth 在社交媒体应用上相当流行。你很有可能对如下的请求非常熟悉:
通过 OAuth,Spotify(客户端)能在不提供证书的情况下代表 Bob(资源所有者)访问 Facebook(资源服务端)
当收到这样一个弹出窗口时,OAuth 协议在后台运行如下流程:
赋予 Spotify 的对 Facebook 数据的委托访问
Spotify 发送一条信息给 Bob,请求访问他的个人主页、好友列表、邮箱和生日的权限。
Bob 给 Spotify 提供授权来收集所需要的数据。
Spotify 将授权发送给 Facebook API(应用程序接口)。
Facebook API 验证授权,并且给 Spotify 发送访问令牌来访问受保护的资源。
Spotify 将访问令牌发送给授权服务器所提供的另一个 Facebook API
Facebook API 将所请求的数据发送给 Spotify
通过使用授权和令牌(我们将在下文讨论),Spotify 获得了接触 Bob 的 Facebook 账号的“代泊钥匙卡”。
对底层一探究竟
上文从非常概略的角度解释了 OAuth 如何工作。现在让我们来看看它的内部,了解使协议能够运作的各个部分。
在上一节中,我们讨论了 OAuth 的抽象设计:客户端、资源所有者、授权服务端和资源服务端。在这个系统中还有如下部分:
范围(Scope)和令牌(Token)
授权(Grant)和流程(Flow)
范围和令牌
范围和令牌是 OAuth 中用于实现细粒度的访问控制的。它们在一起使用时表示了“做某事的许可”。令牌是关于权限的部分,定义了“做某事”中的某事是什么。想象电影票的例子:范围就是你被授权观看的电影名称,这张电影票就是令牌,只有电影院的员工可以验证它的真假。
在我们特斯拉的例子中,范围是使用代泊钥匙启动车辆时车辆被允许启动的功能,令牌就是代泊钥匙本身,只有在钥匙是被特斯拉经销商发行的情况下,特斯拉车辆才能识别该钥匙。
回到图 2,我们可以看到授权服务端和资源服务端有不同的 API。从功能上而言,授权服务端用于验证和授权客户端,而资源服务端托管被请求的资源。为了使资源服务端知道是否接受某个查询信息,它必须知道这个请求者是否已被授权。这就是令牌的来源。令牌的存在是为了通知资源服务端,该请求者已被授权服务端审查过了,并且有权限发送这个请求。通过使用令牌为代理,这种提供证书的需要就被抽象化了。访问令牌通常作为 JWT 承载令牌发布,这些令牌被数字签名成对客户端无意义的数据,但当资源服务端解码后,它们包含了重要的信息。被包含在访问令牌中的其中一个变量就是它的范围。
由于范围限制了允许的操作,因此他们必须由资源(例如 Facebook)所固有的范围去定义。按照惯例,范围有四种:
可读访问
可写访问
可读可写访问
无法访问
范围可以要么获得某个数据、要么做某件事、要么既获得某个数据又做某件事、或者什么都不干。上文中,Facebook 和 Spotify 的例子就是一个可读访问的实例,而路透社要求发布一条文章的推特就是可写访问。定义范围是用于指定第三方如何访问用户数据的一个非常强大的工具。
要理解如何使用范围,可以阅读Slack和Google这些公司的文档,它们展示了范围参数的不同组合。
有另一种令牌叫做刷新令牌。它们不是访问资源的“钥匙”,但是可以在令牌失效时(例如,令牌过期)用于自动获得新的访问令牌。
像 Facebook 这样的应用程序能通过强制使用可选的刷新令牌来定期验证授权,从而获得新的访问令牌,以实现更高级别的安全性。刷新令牌具有一个重要功能:可以通过使令牌失效和回退客户端对权限资源的访问,来撤回令牌。
授权和流程
授权指示了客户端要获取访问令牌的操作顺序。这些独特的顺序称为流程。回到我们电影院的例子,有两种方式可以获得门票:
在剧场购买
在网上购买
您选择的方法决定了要获取票证,你需要做什么。在剧院购买可能类似于:
开车到电影院附近
进入电影院
走到售票前台
选择放映时间
向影院工作人员提供信用卡(中国或是用支付宝、微信支付等支付工具)
给收据签名(在中国,这一步略过)
获得实体的电影票
而在网上购买需要完成这些步骤:
登录电影院网站
选择放映时间
点击购物车结账
输入付款信息
通过电子邮件获得电子的电影票
如你所见,授权与令牌是不同的,授权不是实质性的事情,但是指示了要操作的流程。我们已经介绍了一种授权方式:刷新令牌(请参见图 3)。不同的授权针对特定的用例作出了优化,并且会影响到终端用户如何和客户端应用程序进行交互,这个过程中应用了各类保密措施。了解构成每种授权类型的不同参数很重要,但是这会使本文变成一篇中篇小说。所以,在下面的 Github 案例分析中,我们将重点介绍最常见的授权类型:授权码授权 (Authorization Code Grant)。
回想一下,OAuth 通过 HTTPS 运行。资源所有者,客户端和授权服务端之间的所有通信都是通过 URI 进行的。这些 URI 是带有参数的请求,这些参数是这个字符串的一部分。这些参数包含了授权服务端需要了解的要遵循的流程信息。此时,务必要注意的是,有两种客户端:机密客户端和公共客户端。我们可以信任机密客户端有能力秘密地持有对访问资源至关重要的令牌。这种客户端可以是服务端应用程序。我们不能信任公共客户端能够存储客户证书。这种客户端包括移动端或 Javascript 浏览器应用程序。
授权码授权
授权码授权可能是最常见的授权类型(参见图 4)。从本质上来说,客户端接收由授权服务端发布的唯一代码,这个代码在接下来的流程中可以用于交换令牌。通过将接受令牌所需的步骤分解成两个不同的请求,授权服务端能够在发布令牌之前验证关于客户端的重要信息。
带有 PKCE 扩展的授权码授权
这是授权码授权的一个变体。它用于那些没有能力存储证书的公共客户端。客户端和服务端通过 PKCE 扩展(用于交换代码的公钥)来传递一个哈希值,这个哈希值验证了通信没有被拦截。
设备码
这种授权方式的扩展被用于一类通过互联网相互连接的特殊设备:这些设备无法使用浏览器,或者有非常糟糕的键盘体验,比如通过游戏机手柄或者虚拟键盘来登录游戏页面。如果你曾经在智能电视登录过流媒体账号,你可能早已体验过这个流程。
案例学习:GitHub 的单点登录(SSO)
将所有这些概念放在一起,我们来学习一个案例。Teleport是一个开源的远程访问工具,能够使用户通过 GitHub 的单点登录(Single Sign-On, SSO)来登录,这其中就使用了 OAuth。
首先,我们来定义一些专业词汇:
客户端:Teleport
资源拥有者:Teleport 用户
授权服务端:Github 授权服务器(GAS)
资源服务端:GitHub 资源服务器(GRS)
现在我们就可以开始了!如上文所述,我们将遵循授权码授权。这是因为令牌所有者是一个用户,而客户端是一个服务端应用程序。Teleport 不被托管,所以假设我们已经安全地将它安装在你的架构中了。如果不是这样的话,应该使用 PKCE 扩展的授权码授权。
授权码授权流程
这个流程如下所示:
第一步:Teleport 用户访问 Teleport 程序
第二步:Teleport 提示 Teleport 用户通过 GitHub SSO 登录
第三步:Teleport 用户点击“登录“,然后被通过如下参数重定向,这些参数被包括在 HTTPS GET 请求中:
authorization_server
response_type=code
client_id
redirect_uri
scope
state
这些参数是什么意思呢?
authorization_server
:是 GAS 公开的 URL。所有资源应用程序会提供一个 URL 来重定向,通常是一个 API。在 GitHub 的例子中,这个 API 是 https://github.com/login/oauth/authorizeresponse_type=code
:会让 GAS 知道 Teleport 希望获得一个授权码client_id
:提供了一个字符串给 GAS,GAS 可以根据授权客户端的注册表来检查。像 GitHub 这样的应用程序将要求客户端注册,来帮助识别。让我们使用 12345 作为 ID。redirect_uri
:告知 GAS 通过这个 URL 来将 Teleport 用户重定向回到 Teleport,并且有所有 Teleport 所需要的变量。在这个例子中,我们可以将 Gravitation 文档作为重定向 URL 的实例:https://teleport.example.com:3080/v1/webapi/github/callbacscope
:定义了访问资源的限制。这些范围通常由资源服务端所固有的范围来定义。我们可以在这里看 Github 的范围。当看见 Teleport 的代码库时,我们发现唯一的范围要求就是read:org
,这使得 Teleport 能读取组织的成员信息、团队的成员信息以及组织的项目。state
:是一个由 Teleport 随机生成的字符串,用于在 Teleport 和授权服务端之间来回传递。通过传递这个字符串,客户端和授权服务端都能知道它们是跟同一个设备进行交流。在这个例子中,我们假设 state 字符串是 syl(我的狗的名字)。
把这些参数都放在一起,这个 Teleport 用户在接受登录提示后被重定向的 URL 是这样的:
第四步:一旦 GAS 收到了这个请求,它将在一个 Teleport 的注册表中验证这个client_id
。知道 Teleport 要求返回授权码后,GAS 会给用户发送一个重定向的 URL,这个 URL 中有这个授权码和已经通过的 state 参数。我们下一个 URL 是这样的;
https://teleport.example.com:3080/v1/webapi/github/callback?code=pkzdZumQi1&state=syl
第五步:在收到授权码之后,Teleport 会自动查询 GAS,通过授权码来交换令牌,其中,code、redirect_url、client_id 这些参数必须包含在查询中。还需要两个额外的参数:
grant_type=authorization_code
告知 GAS 使用授权码流程client_secret
是出自客户端注册过程中的 GitHub 端。这个字符串应该是一个密钥并且不能被公开访问。因为 Teleport 是被托管在我们自己的基础架构中,我们知道我们的架构是安全的,所以我们乐意传递这个参数。另一方面,我们应该使用 PKCE 扩展并对这个生成的字符串进行哈希加密处理。在这个例子中,我们的密钥是 gravitational。
回想一下,用授权码来交换令牌需要一个 POST 请求。将这些放在一起,Teleport 将会发送一个这样的请求:
第六步:使用client_secret
和code
,授权服务端能够验证 Teleport 客户端的请求,并且发布一个 JWT 承载令牌,这个令牌将范围和到期时间编码在内(可以将刷新令牌也包含在内)。这个令牌是这样的:
第七步:现在,我们获得了访问令牌。剩下的步骤就是代表 Teleport 用户向发出 API 请求并接受所请求的资源。为此,我们在 HTTPS 授权标头中将访问令牌作为 Bearer 凭证传递。回想一下我们的范围为read:org
,这意味着我们只能调用很少的一部分方法。考虑到这一点,我们的标头可能类似于:
第八步:Github API 发送 Teleport 用户的组织成员信息给 Teleport
结论
如果你完成了所有步骤,恭喜你!尽管 OAuth 提供了这种容易被忽略的便利,它也仍然是一个复杂的协议,需要花费一些时间来实现。我们刚刚展示的例子是百种 OAuth 流程排列中可能的其中一种。在现在,我希望你对这个概念有了全面而充分的理解,使你能够有自信自己继续探索这个协议。
原文链接:
https://gravitational.com/blog/everything-you-need-to-know-about-oauth
评论