使用 OAuth 2.0 进行用户验证

OAuth 2.0 规范定义了一个委托协议,它适用于在支持网络的应用程序和 API 网络中传达授权决策。OAuth 用于各种应用程序中,包括提供用于用户身份验证的机制。这导致许多开发人员和 API 提供商错误地得出结论:OAuth 本身就是一个身份验证协议,并错误地将其用作身份验证协议。我们再强调一下,为了清楚起见

OAuth 2.0 不是身份验证协议。

大多数的困惑来自于 OAuth 在身份验证协议内部使用的内容,开发人员会看到 OAuth 组件,并会与 OAuth 流程交互,并假设通过简单地使用 OAuth,就能完成用户身份验证。事实证明,这不仅不真实,而且对服务提供商、开发人员和最终用户来说也是危险的。

本文旨在帮助潜在身份提供商解决这样一个问题:如何使用 OAuth 2.0 作为基础来构建身份验证和 ID API。基本上,如果你说“我已获得 OAuth 2.0,而且我需要身份验证和 ID”,那么,请继续阅读。

什么是身份验证?

在用户访问应用程序的上下文中,身份验证会告诉应用程序当前的用户是谁,以及用户是否处于活动状态。完整的身份验证协议可能会告诉你有关此用户的一些属性,例如唯一标识符、电子邮件地址,以及当应用程序提示“早安”时如何称呼用户。身份验证主要是与用户及其在应用程序中的处于活动状态有关,而且一项互联网级身份验证协议需要能够跨网络和安全边界执行此操作。

然而,OAuth 并没有告知应用程序这些信息。OAuth 绝对不会提及用户,也不会告知用户如何证明他们的存在,甚至不会告知他们是否仍然存在。就 OAuth 客户端而言,它请求了一个令牌,得到了一个令牌,并且最终使用该令牌访问某些 API。它不知道谁授权了该应用程序,甚至不知道是否有用户。事实上,OAuth 中很重要的一点是,授予这种委托访问权限,可在客户端和被访问资源之间建立连接的情况下使用,而用户不存在。这对于客户端授权来说非常好,但对于身份验证来说则非常糟糕,因为身份验证的重点是判断用户是否存在(以及他们是何人)。

作为我们主题的另一个混淆因素,OAuth 进程通常在其进程中包含几种类型的身份验证:资源所有者在授权步骤中对授权服务器进行身份验证,客户端在令牌端点中对授权服务器进行身份验证,可能还有其他身份验证。OAuth 协议中这些身份验证事件的存在并不意味着 OAuth 协议本身能够可靠地传达身份验证。

然而事实证明,有一些少量的东西可以与 OAuth 一起使用,以创建一种基于这种委托和授权协议的身份验证和身份协议。在几乎所有这些情况下,OAuth 的核心功能仍然保持不变,而发生的事情是用户将对他们身份的访问权委托给其试图登录到的应用程序。然后客户端应用程序成为身份 API 的使用者,从而找出最初授权了该客户端的人是谁。以这种方式在授权基础上构建身份验证的一大好处是,它允许管理最终用户同意,这在互联网规模的跨域身份联合中非常重要。另一个重要的优点是,用户可以同时委托访问其他受保护的 API以及他们的身份,这使得应用程序开发人员和最终用户管理起来更加简单。通过一个调用,应用程序可以了解用户是否已登录,应用程序应该如何称呼用户,下载照片以便打印,并将更新发布到他们的消息流中。这种简单性非常诱人,但同时进行这两项操作,许多开发人员会混淆这两个功能。

身份验证 vs. 授权:一个比喻

为简化理解,用一个比喻来思考这个问题会有所帮助:巧克力和软糖。从一开始,这两者的性质完全不同:巧克力是一种配料,软糖是一种糖果。巧克力可以用来制作许多不同的东西,甚至也可以单独使用。制作软糖时可以加入许多不同类型的材料,其中一种材料可能是巧克力,但想要制作软糖,需要的不仅仅是一种配料,甚至可能根本不涉及巧克力。因此,说巧克力等于软糖是不正确的,说巧克力等于巧克力软糖就更是过于牵强了。

在这个比喻中,OAuth 是巧克力。它是一种多功能配料,是许多不同事物的基础,甚至可以单独使用达到很好的效果。验证更像是软糖。至少需要以正确的方式将几种配料组合在一起才能让它起作用,OAuth 可以是其中一种配料(可能是主要配料),但它并不是必须要的。需要一个食谱来说明如何组合以及如何将它们组合在一起,有很多食谱说明如何实现目标。

事实上,有很多众所周知的食谱可以用来针对特定提供商执行此操作,例如 Facebook Connect、使用 Twitter 登录 和 OpenID Connect(它支持 Google 的登录系统等)。这些食谱都可以在 OAuth 中添加多个项目,例如通用个人资料 API,用来创建验证协议。你可以在不使用 OAuth 的情况下构建验证协议吗?当然可以,就像市面上有各种类型的非巧克力软糖一样。但我们今天在这里要讨论的是专门建立在 OAuth 2.0 上的验证、可能出现的问题以及如何确保它安全有效。

使用 OAuth 验证时的常见陷阱

尽管使用 OAuth 构建验证协议完全有可能,但仍有很多事情会让构建者踩坑,无论是在身份提供商方面还是在身份使用者方面。本文所述的做法旨在告知潜在的身份提供商有关常见风险以及告知消费者有关使用基于 OAuth 的验证系统时可以避免的常见错误。

访问令牌作为验证证明

身份验证通常发生在访问令牌签发之前,因此很自然会认为接收任何类型的访问令牌即是对此类身份验证的证明。但是,单纯拥有访问令牌并不能向客户端说明任何问题。在 OAuth 中,令牌被设计为对客户端不透明,但在用户身份验证的上下文中,客户端需要能够从令牌中提取一些信息。

此问题源于客户端并非 OAuth 访问令牌的目标受众。相反,它是该令牌的授权出示人,而受众实际上是受保护的资源。受保护的资源通常不能根据令牌单独判断用户是否仍然存在,因为根据 OAuth 协议的本质和设计,用户在客户端和受保护的资源之间的连接上不可用。为了解决这个问题,需要有一个针对客户端本身的人工制品。这可以通过双重用途访问令牌来实现,定义一种格式供客户端解析和理解。但是,由于常规 OAuth 并未定义访问令牌本身的特定格式或结构,因此 OpenID Connect 的 ID 令牌和 Facebook Connect 的 Signed Response 等协议在访问令牌旁边提供辅助令牌,直接将身份验证信息传达给客户端。这允许主访问令牌保持对客户端不透明,就像在常规 OAuth 中一样。

受保护的 API 访问作为身份验证的证明

由于访问令牌可以交换为一组用户属性,因此很容易认为拥有有效的访问令牌就足以证明用户已通过身份验证。在某些情况下,此假设为真,其中在鉴权服务器对用户进行身份验证的情况下,令牌是新创建的。但是,这不是在 OAuth 中获取访问令牌的唯一方法。即使用户不在场,也可以使用刷新令牌和断言获取访问令牌,在某些情况下,访问授权可以在无需用户进行身份验证的情况下发生。

此外,访问令牌通常在用户不再存在之后仍然可以使用很长时间。请记住,由于 OAuth 是委托协议,因此这是其设计的根本原因。这意味着,如果客户端想确保身份验证仍然有效,仅仅再次交换令牌以获取用户的属性还不足以,因为 OAuth 受保护的资源和身份 API 通常无法判断用户是否存在。

访问令牌注入

当客户端接受除令牌端点返回调用之外的其他来源的访问令牌时,会发生额外的(而且非常危险的)威胁。客户端在使用隐式流(在此流中,令牌作为 URL 哈希中的参数直接传入)和未正确使用 OAuth“状态”参数时,可能发生这种情况。如果应用程序的不同部分在组件之间传入访问令牌,以便在它们之间“共享”访问权限,则也会发生此问题。这是有问题的,因为它为外部方潜在地向应用程序注入访问令牌(并可能向应用程序外部泄露)提供了途径。如果客户端应用程序不通过某种机制验证访问令牌,则它无法区分有效令牌和攻击令牌。

可以通过使用授权码流仅直接从授权服务器的令牌端点接受令牌来缓解此问题,还可以通过使用攻击者无法猜测的“状态”值来缓解此问题。

缺乏受众限制

使用访问令牌换取一组属性以获取当前用户时的另一个问题是,大多数 OAuth API 并未为返回的信息提供任何受众限制机制。换句话说,完全有可能拿出一个朴素的客户端,向其提供来自另一个客户端的(有效)令牌,并且让朴素客户端将其视为“登录”事件。毕竟,令牌是有效的,并且对 API 的调用将返回有效的用户信息。当然,问题在于用户没有采取任何措施来证明他们在此,并且在这种情况下,他们甚至没有授权朴素的客户端。

可以通过将身份验证信息与客户端可以识别和验证的身份标识符一起传达给客户端来缓解此问题,从而让客户端能够区分针对它自身的身份验证与针对另一个应用程序的身份验证。还可以通过在 OAuth 过程中将该组身份验证信息直接传入客户端(而不是通过次要机制(例如受 OAuth 保护的 API))来缓解此问题,从而防止客户端在该过程中后期拥有未知且不受信任的信息组。

插入无效用户信息

如果攻击者能够截取或征用客户端的一个调用,则可以在客户端无法得知任何情况不正常时更改返回的用户信息的内容。这会让攻击者能够通过在正确的调用序列中简单地交换用户标识符来假冒朴素客户端中的用户。可以通过在身份验证协议流程期间(例如,在 OAuth 令牌旁)直接从身份提供者那里获取身份验证信息并通过可验证签名保护身份验证信息来缓解此问题。

对于每个潜在身份提供者,不同的协议

基于 OAuth 的身份 API 面临的最严重的问题之一是,即使使用符合全部标准的 OAuth 机制,不同的提供程序也会不可避免地以不同的方式实现实际身份 API 的详细信息。例如,用户标识符可能存在于一个提供程序中的 `user_id` 字段中,但存在于另一个提供程序中的 `subject` 字段中。即使这些内容在语义上是等效的,它们也需要两个独立的代码路径来处理。换句话说,虽然授权可能在每个提供程序中都是通过相同的方式完成,但身份验证信息的传递方式可能不同。这个问题可以通过提供程序使用基于 OAuth 构建的标准认证协议来缓解,以便无论身份信息来自何处,都能够以相同的方式传输。

此问题发生的原因是,此处讨论的用于传递身份验证信息的方法明显超出了 OAuth 的范围。OAuth 未定义 特定令牌格式,未定义 访问令牌的共同作用域集,并且完全没有说明 受保护资源如何验证访问令牌

使用 OAuth 进行用户认证的标准:OpenID Connect

OpenID Connect 是一项开放标准,发布于 2014 年初,定义了一种可互操作的方式来使用 OAuth 2.0 执行用户认证。从本质上说,它是一种广泛发布的认证食谱,经过众多专家反复尝试和测试。应用程序无需为每个潜在的身份提供程序构建不同的协议,而是可以用一种协议与任意数量的提供程序通信。作为一项开放标准,OpenID Connect 可以不受限制或知识产权问题地由任何人实施。

OpenID Connect 直接构建在 OAuth 2.0 之上,在大多数情况下都与 OAuth 基础设施一起部署(或在之上部署)。OpenID Connect 还使用 JSON 对象签署和加密 (JOSE) 规范套件,用于在不同位置传输已签名和已加密的信息。事实上,带有 JOSE 功能的 OAuth 2.0 部署已经非常接近于定义完全兼容的 OpenID Connect 系统,而两者之间的差别相对较小。但这种差别产生了巨大的影响,OpenID Connect 能够通过向 OAuth 基础添加若干关键组件来避免上述许多陷阱

ID 令牌

OpenID Connect ID令牌是一个已签名的JSON Web令牌 (JWT),与常规OAuth访问令牌一同提供给客户端应用程序。ID令牌包含有关认证会话的一组声明,包括用户的标识符(sub)、已发布此令牌的标识提供商的标识符(iss)和为其创建此令牌的客户端的标识符(aud)。此外,ID令牌包含有關令牌有效(且通常较短)的寿命以及任何有关向客户端传达的认证环境信息,例如用户多久前获得了主认证机制。由于ID令牌的格式为客户端所知,因此它能够直接解析令牌的内容并获取此信息,而无需依赖外部服务来执行此操作。并且,它与访问令牌一起颁发(而不是代替访问令牌),允许访问令牌对客户端保持不透明,因为它是在常规OAuth中定义的。最后,令牌本身由标识提供商的私钥签名,这在它内部的声明中增加了额外的保护层,此外还有TLS传输保护最初用来获取令牌,从而阻止了身份假冒攻击类别。通过对这个ID令牌应用几个简单的检查,客户端可以保护自己免受许多常见的攻击。

由于ID令牌由授权服务器签名,因此它还提供了一个位置来为授权代码(c_hash)和访问令牌(at_hash)添加独立签名。当授权代码和访问令牌内容对客户端保持不透明时,客户端仍可以验证这些哈希,从而防止一类的注入攻击。

UserInfo端点

需要注意的是,客户端不需要使用访问令牌,因为ID令牌包含处理认证事件所需的所有信息。但是,为了与 OAuth 兼容并匹配并行授权身份和其他 API 访问的总体趋势,OpenID Connect 始终与 OAuth 访问令牌一起颁发 ID 令牌。

除了 ID 令牌中的声明,OpenID Connect 还定义了一个标准的受保护资源,其中包含有关当前用户的声明。如上所述,这里的声明不是身份验证过程的一部分,而是提供捆绑的身份属性,使应用程序开发人员认为身份验证协议更有价值。毕竟,“早上好,Jane Doe”听起来要比“早上好,9XE3-JI34-00132A”好。OpenID Connect 定义了一组标准化的 OAuth 作用域,映射到这些属性的子集:个人资料电子邮件电话地址,允许普通的 OAuth 授权请求传达请求所需的信息。OpenID Connect 定义了一个特殊的openid作用域,可启用 ID 令牌的发布以及访问令牌对 UserInfo Endpoint 的访问。OpenID Connect 作用域可以与其他非 OpenID Connect OAuth 作用域一起使用,而不会发生冲突,同时发出的访问令牌有可能针对不同的受保护资源。这允许 OpenID Connect 身份系统与 OAuth 授权系统平稳共存。

动态服务器发现和客户端注册

OAuth 2.0 的编写是为了允许各种不同的部署,但设计上并未指定这些部署的设置方式或组件如何相互了解。在常规的 OAuth 世界中,一个授权服务器保护一个特定的 API,且两者紧密耦合,这未尝不可。使用 OpenID Connect,一个通用受保护 API 部署在广泛的客户端和提供商中,所有这些客户端和提供商都需要相互了解才能运行。让每个客户端都提前了解每个提供商是不可扩展的,而要求每个提供商了解每个潜在客户端则更不可扩展。

为了解决这个问题,OpenID Connect 定义了一个发现协议,允许客户端轻松地获取有关如何与特定身份提供商交互的信息。在事务的另一端,OpenID Connect 定义了一个客户端注册协议,允许将客户端介绍给新的身份提供商。通过使用这两种机制和一个通用身份 API,OpenID Connect 可以在互联网范围内发挥作用,其中任何一方都不必事先了解彼此。

与 OAuth 2.0 的兼容性

即使拥有所有这些强大的身份验证功能,OpenID Connect(通过设计)仍然与纯 OAuth 2.0 兼容,这使得其成为在 OAuth 系统之上进行部署的绝佳选择,并且对开发人员的工作量影响最小。事实上,如果某个服务已经使用 OAuth 和JSON Object Signing and Encryption (JOSE)规范(包括 JWT),那么该服务在支持 OpenID Connect 方面已经走上了正轨。

为了促进构建优秀的客户端应用程序,OpenID Connect 工作组发布了构建基本 OpenID Connect 客户端的文档,其中涵盖了使用授权码流程以及构建隐式 OpenID Connect 客户端。这两个文档指导开发者构建一个基础的 OAuth 2.0 客户端,并为 OpenID Connect 添加一些必要的组件。

高级能力

虽然核心规范非常直接,但并非所有用例都能通过基础机制得到充分解决。为了支持高级用例(包括更高的安全性部署),OpenID Connect 也定义了超出标准 OAuth 的一些可选高级能力,包括以下能力(以及其他能力):

进一步阅读