博客 | NGINX

使用 NGINX 和 NGINX Plus 验证 OAuth 2.0 访问令牌

NGINX-F5-horiz-black-type-RGB 的一部分
Liam Crilly 缩略图
利亚姆·克里利
2019 年 5 月 13 日发布

 

图片由unsplash.com 上的 John T.提供

有很多用于验证 API 调用的选项,从 X.509 客户端证书到 HTTP 基本身份验证。 然而,近年来,OAuth 2.0访问令牌的形式出现了一种事实上的标准。 这些是从客户端传递到 API 服务器的身份验证凭据,通常以 HTTP 标头的形式携带。

然而,OAuth 2.0 是一个相互关联的标准的迷宫。 发布、呈现和验证 OAuth 2.0 身份验证流程的过程通常依赖于几个相关标准。 在撰写本文时,有八个OAuth 2.0 标准,访问令牌就是一个例子,因为 OAuth 2.0 核心规范( RFC 6749 )没有指定访问令牌的格式。 在现实世界中,有两种常用的格式:

  • RFC 7519定义的 JSON Web Token (JWT)
  • 不透明令牌只不过是经过身份验证的客户端的唯一标识符

经过身份验证后,客户端会在每次 HTTP 请求中出示其访问令牌,以获得对受保护资源的访问权限。 需要验证访问令牌以确保它确实是由受信任的身份提供者 (IdP) 颁发的,并且没有过期。 由于 IdP 对其颁发的 JWT 进行加密签名,因此可以“离线”验证 JWT,而无需在运行时依赖 IdP。 通常,JWT 还包括一个可检查的到期日期。 NGINX Plus auth_jwt模块执行离线 JWT 验证。

另一方面,不透明令牌必须通过将它们发送回颁发它们的 IdP 来进行验证。 然而,这样做的好处是,此类令牌可以被 IdP 撤销,例如作为全局注销操作的一部分,而不会使之前登录的会话仍然处于活动状态。 全局注销可能还需要使用 IdP 验证 JWT。

在此博客中,我们描述了 NGINX 和 NGINX Plus 如何充当 OAuth 2.0依赖方,将访问令牌发送给 IdP 进行验证,并且仅代理通过验证过程的请求。 我们讨论了使用 NGINX 和 NGINX Plus 执行此任务的各种好处,以及如何通过短时间缓存验证响应来改善用户体验。 对于 NGINX Plus,我们还展示了如何通过使用 JavaScript 模块更新键值存储,将缓存分布在 NGINX Plus 实例集群中,如NGINX Plus R18中介绍的那样。

除非另有说明,本博客中的信息适用于 NGINX Open Source 和 NGINX Plus。 对 NGINX Plus 的引用仅适用于该产品。

令牌自省

使用 IdP 验证访问令牌的标准方法称为令牌自检RFC 7662OAuth 2.0 令牌自检,现在是一个得到广泛支持的标准,它描述了依赖方用来向 IdP 提供令牌的 JSON/REST 接口,并描述了响应的结构。 它受到许多领先的 IdP 供应商和云提供商的支持。

无论使用哪种令牌格式,在每个后端服务或应用上执行验证都会导致大量重复的代码和不必要的处理。 需要考虑各种错误情况和边缘情况,并且在每个后端服务中这样做会导致实施不一致,从而导致不可预测的用户体验。 考虑每个后端服务如何处理以下错误情况:

  • 缺少访问令牌
  • 极大的访问令牌
  • 访问令牌中的字符无效或意外
  • 呈现多个访问令牌
  • 后端服务之间的时钟偏差
执行令牌验证的后端应用

使用 NGINX auth_request模块验证令牌

为了避免代码重复和由此产生的问题,我们可以使用 NGINX 代表后端服务验证访问令牌。 这有许多好处:

  • 仅当客户端提供有效令牌时,请求才会到达后端服务
  • 可以使用访问令牌保护现有的后端服务,而无需更改代码
  • 只有 NGINX 实例(不是每个应用程序)需要向 IdP 注册
  • 对于每种错误情况,行为都是一致的,包括丢失或无效的令牌
NGINX 作为反向代理执行令牌验证

使用 NGINX 充当一个或多个应用的反向代理,我们可以使用auth_request模块在将请求代理到后端之前触发对 IdP 的 API 调用。 我们马上就会看到,下面的解决方案有一个根本缺陷,但它引入了auth_request模块的基本操作,我们将在后面的章节中对其进行扩展。

 

auth_request指令(第 5 行)指定处理 API 调用的位置。 仅当auth_request响应成功时,才会代理到后端(第 6 行)。 auth_request位置在第 9 行定义。 它被标记为内部,以防止外部客户端直接访问它。

第 11-14 行定义请求的各种属性,以使其符合令牌自省请求格式。 请注意,自省请求中发送的访问令牌是第 14 行定义的主体的组成部分。 这里token=$http_apikey表示客户端必须在apikey请求标头中提供访问令牌。 当然,访问令牌可以在请求的任何属性中提供,在这种情况下,我们使用不同的 NGINX 变量。

使用 NGINX JavaScript 模块扩展auth_request

如上所述,以这种方式使用auth_request模块不是一个完整的解决方案。 auth_request模块使用 HTTP 状态代码来确定成功( 2xx = 好, 4xx =糟糕)。 但是,OAuth 2.0 令牌自检响应在 JSON 对象中编码成功或失败,并返回 HTTP 状态代码200(好的)两种情况都是如此。

有效令牌的令牌自检响应的 JSON 格式

我们需要一个 JSON 解析器将 IdP 的自省响应转换为适当的 HTTP 状态代码,以便auth_request模块可以正确解释该响应。

值得庆幸的是,JSON 解析对于 NGINX JavaScript 模块(njs)来说是一项简单的任务。 因此,我们不需要定义位置块来执行令牌自检请求,而是告诉auth_request模块调用 JavaScript 函数。

[编辑——这篇文章是探讨 NGINX JavaScript 模块用例的几篇文章之一。 有关完整列表,请参阅NGINX JavaScript 模块的用例

本节中的代码已更新为使用js_import指令,该指令取代了 NGINX Plus R23 及更高版本中的js_include指令。 有关更多信息,请参阅NGINX JavaScript 模块的参考文档 -示例配置部分展示了 NGINX 配置和 JavaScript 文件的正确语法。 ]

笔记: 该解决方案要求使用nginx.conf中的load_module指令将 JavaScript 模块作为动态模块加载。 有关说明,请参阅NGINX Plus 管理指南

 

第 13 行的js_content指令指定 JavaScript 函数introspectAccessToken作为auth_request处理程序。 处理程序函数在oauth2.js中定义:

 

请注意, introspectAccessToken函数向另一个位置( /oauth2_send_request )发出 HTTP 子请求(第 2 行),该位置在下面的配置片段中定义。 然后,JavaScript 代码解析响应(第 5 行),并根据active字段的值将适当的状态代码发送回auth_request模块。 有效(活动)令牌返回HTTP204 (无内容 (但成功)并且无效令牌返回HTTP403 (禁忌) 错误条件返回HTTP401 (未经授权)以便可以将错误与无效令牌区分开来。

笔记: 此代码仅作为概念证明提供,不具有生产质量。 下面提供了具有全面错误处理和日志记录的完整解决方案。

第 2 行定义的子请求目标位置看起来非常类似于我们原始的auth_request配置。

 

构建令牌自检请求的所有配置都包含在/_oauth2_send_request位置内。 身份验证(第 19 行)、访问令牌本身(第 21 行)以及令牌自省端点的 URL(第 22 行)通常是唯一必要的配置项。 IdP 需要进行身份验证才能接受来自此 NGINX 实例的令牌自检请求。 OAuth 2.0 Token Introspection规范要求进行身份验证,但未指定方法。 在此示例中,我们在授权标头中使用了承载令牌。

有了这个配置,当 NGINX 收到请求时,它会将其传递给 JavaScript 模块,该模块会对 IdP 发出令牌自检请求。 检查来自 IdP 的响应,当active字段为true时,身份验证即视为成功。 该解决方案是使用 NGINX 执行 OAuth 2.0 令牌自检的一种紧凑而有效的方式,并且可以轻松地适应其他身份验证 API。

但我们还没有完成。 总体而言,令牌自检的最大挑战是它会增加每个 HTTP 请求的延迟。 当所涉及的 IdP 是托管解决方案或云提供商时,这可能会成为一个重大问题。 NGINX 和 NGINX Plus 可以通过缓存内省响应来优化这个缺点。

优化一: NGINX 缓存

OAuth 2.0 令牌自检由 IdP 在 JSON/REST 端点提供,因此标准响应是带有 HTTP 状态的 JSON 主体200。 当此响应与访问令牌相对应时,它变得高度可缓存。

完成令牌自检响应,以获得有效令牌

NGINX 可以配置为缓存每个访问令牌的自省响应副本,以便下次提供相同的访问令牌时,NGINX 会提供缓存的自省响应,而不是向 IdP 发出 API 调用。 这极大地改善了后续请求的总体延迟。 我们可以控制缓存响应的使用时间,以减轻接受过期或最近撤销的访问令牌的风险。 例如,如果 API 客户端通常在短时间内发出多个 API 调用,那么 10 秒的缓存有效期可能足以显著改善用户体验。

通过指定其存储来启用缓存——磁盘上用于缓存(自省响应)的目录和用于密钥(访问令牌)的共享内存区域。

 

proxy_cache_path指令分配了必要的存储: /var/cache/nginx/oauth用于存储自省响应,以及一个名为token_responses的内存区域用于存储密钥。 它是在http上下文中配置的,因此出现在服务器位置块之外。 然后在处理令牌自省响应的位置块内启用缓存本身:

 

使用proxy_cache指令 (第 26 行) 为该位置启用缓存。 默认情况下,NGINX 根据 URI 进行缓存,但在我们的例子中,我们希望根据apikey请求标头中显示的访问令牌来缓存响应(第 27 行)。

在第 28 行,我们使用proxy_cache_lock指令告诉 NGINX,如果并发请求使用相同的缓存键到达,则需要等到第一个请求填充缓存后才能响应其他请求。 proxy_cache_valid指令(第 29 行)告诉 NGINX 缓存内省响应多长时间。 如果没有此指令,NGINX 将根据 IdP 发送的缓存控制标头确定缓存时间;然而,这些标头并不总是可靠的,这就是为什么我们还告诉 NGINX忽略那些会影响我们如何缓存响应的标头(第 30 行)。

现在启用缓存后,提供访问令牌的客户端仅会每 10 秒发出一次令牌自检请求而产生延迟成本。

优化2: 使用 NGINX Plus 进行分布式缓存

将内容缓存与令牌自省相结合是提高整体应用性能的一种非常有效的方法,而且对安全性的影响可以忽略不计。 但是,如果 NGINX 以分布式方式部署(例如,跨多个数据中心、云平台或主动主动集群),则缓存的令牌自省响应仅适用于执行自省请求的 NGINX 实例。

使用 NGINX Plus,我们可以使用keyval模块(内存中的键值存储)来缓存令牌自省响应。 此外,我们还可以使用zone_sync模块在 NGINX Plus 实例集群之间同步这些响应。 这意味着无论哪个 NGINX Plus 实例执行了令牌自检请求,响应都可以在集群中的所有 NGINX Plus 实例上获得。

笔记: 用于运行时状态共享的zone_sync模块的配置超出了本博客的范围。 有关在 NGINX Plus 集群中共享状态的更多信息,请参阅NGINX Plus 管理指南

NGINX Plus R18及更高版本中,可以通过修改keyval指令中声明的变量来更新键值存储。 由于 JavaScript 模块可以访问所有 NGINX 变量,因此允许在处理响应期间将内省响应填充到键值存储中。

与 NGINX 文件系统缓存一样,键值存储是通过指定其存储来启用的,在本例中是存储键(访问令牌)和值(自省响应)的内存区域。

 

请注意,使用keyval_zone指令的超时参数,我们为缓存响应指定了与auth_request_cache.conf第 29 行相同的 10 秒有效期,以便 NGINX Plus 集群的每个成员在响应过期时独立删除该响应。 第 2 行指定每个条目的键值对:键是apikey请求标头中提供的访问令牌,值是$token_data变量评估的自省响应。

现在,对于每个包含apikey请求标头的请求, $token_data变量都会填充先前的令牌自省响应(如果有)。 因此,我们更新 JavaScript 代码来检查我们是否已经有令牌自省响应。

 

第 2 行测试此访问令牌是否已有键值存储条目。 因为有两种路径可以获得自省响应(从键值存储或从自省响应),所以我们将验证逻辑移至以下单独的函数tokenResult

 

现在,每个令牌自省响应都保存到键值存储中,并在 NGINX Plus 集群的所有其他成员之间同步。 以下示例显示了一个带有有效访问令牌的简单 HTTP 请求,然后查询 NGINX Plus API 以显示键值存储的内容。

$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/ HTTP/1.1 200 OK 日期: 2019 年 4 月 24 日星期三 17:41:34 GMT 内容类型:应用/json 内容长度: 612 $ curl http://localhost/api/4/http/keyvals/access_tokens {“tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA”:“{\”active\“:true}”}

请注意,键值存储本身使用 JSON 格式,因此令牌自省响应会自动对引号应用转义。

优化三: 从自省响应中提取属性

OAuth 2.0 令牌自省的一个有用功能是,响应除了包含令牌的活动状态之外,还可以包含有关令牌的信息。 此类信息包括令牌到期日期和相关用户的属性:用户名、电子邮件地址等。

带有令牌属性的令牌自检响应

这些额外的信息可能非常有用。 它可以被记录,用于实现细粒度的访问控制策略,或提供给后端应用。 我们可以将这些属性中的每一个导出到auth_request模块,方法是将它们作为附加响应标头发送,并发送成功的 (HTTP204 ) 回复。

 

我们迭代自省响应的每个属性(第 23 行),并将其作为响应头发送回auth_request模块。 每个标头名称都以Token-为前缀,以避免与标准响应标头冲突(第 26 行)。 这些响应标头现在可以转换为 NGINX 变量并用作常规配置的一部分。

 

在这个例子中,我们将用户名属性转换为一个新变量$username (第 11 行)。 auth_request_set指令使我们能够将令牌自省响应的上下文导出到当前请求的上下文中。 每个属性的响应标头(由 JavaScript 代码添加)可作为$sent_http_token_ attribute获得。 然后,第 12 行将$username的值作为代理到后端的请求标头包含在内。 我们可以对令牌自省响应中返回的任何属性重复此配置。

将令牌自检响应中的属性导出到代理请求

生产配置

上述代码和配置示例是实用的,适合概念验证测试或针对特定用例进行定制。 对于生产用途,我们强烈建议增加额外的错误处理、日志记录和灵活的配置。 您可以在我们的 GitHub 仓库中找到 NGINX 和 NGINX Plus 的更强大、更详细的实现:

概括

在此博客中,我们展示了如何结合使用 NGINX auth_request模块与 JavaScript 模块对客户端请求执行 OAuth 2.0 令牌自检。 此外,我们还通过缓存扩展了该解决方案,并从自省响应中提取了属性以用于 NGINX 配置。

我们还描述了如何将 NGINX Plus 键值存储用作自省响应的分布式缓存,适用于跨 NGINX Plus 实例集群的生产部署。

亲自尝试使用 NGINX Plus 进行 OAuth 2.0 令牌自省 - 立即开始30 天免费试用联系我们讨论您的用例


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”