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

图片由 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 令牌 (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 7662(OAuth 2.0 令牌自省)现已成为广泛支持的标准,它描述了依赖方用来执行以下操作的 JSON/REST 接口:向 IdP 提供令牌,并描述响应的结构。它得到许多领先的 IdP 供应商和云提供商的支持。

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

  • 缺少访问令牌
  • 极大的访问令牌
  • 访问令牌中存在无效或意外的字符
  • 提供多个访问令牌
  • 后端服务之间的时钟偏差

执行令牌验证的后端应用程序

使用 NGINX auth_request 模块验证令牌

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

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

NGINX 作为反向代理执行令牌验证

通过 NGINX 充当一个或多个应用程序的反向代理,我们可以使用 auth_request 模块在将请求代理到后端之前触发对 IdP 的 API 调用。正如我们稍后将看到的,以下解决方案有一个根本性缺陷,但它介绍了 auth_request 模块的基本操作,我们将在后面的部分中对其进行扩展。

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

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

使用 NGINX JavaScript 模块扩展 auth_request

如上所述,以这种方式使用 auth_request 模块并不是一个完整的解决方案。自动h_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 行),并根据 a 的值将适当的状态代码发送回 auth_request 模块。活跃领域。有效(活动)令牌返回 HTTP 204(无内容)(但成功),无效令牌返回 HTTP 403(禁止)。错误情况会返回 HTTP 401(未经授权),以便将错误与无效令牌区分开来。

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

第 2 行中定义的子请求目标位置与我们原始的 auth_request 配置非常相似。

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

完成此配置后,当 NGINX 收到请求时,会将其传递给 JavaScript 模块,该模块会针对 IdP 发出令牌内省请求。检查来自 IdP 的响应,当活动字段为 true 时,身份验证被视为成功。该解决方案是一种使用 NGINX 执行 OAuth 2.0 令牌自省的紧凑而高效的方法,并且可以轻松适应其他身份验证 API。

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

优化1:NGINX缓存

OAuth 2.0 令牌自省由 IdP 在 JSON/REST 端点提供,因此标准响应是 HTTP 状态 200 的 JSON 正文。当此响应针对访问令牌进行键入时,它变得高度可缓存。

有效令牌的完整令牌内省响应

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

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

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

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

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

现在启用缓存后,提供访问令牌的客户端只需承受每 10 秒发出一次令牌自省请求的延迟成本。

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

将内容缓存与令牌内省相结合是提高整体应用程序性能的一种非常有效的方法,而且对安全性的影响可以忽略不计。但是,如果 NGINX 以分布式方式部署(例如,跨多个数据中心、云平台或双活集群),则缓存到ken 内省响应仅适用于执行内省请求的 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 指令的 timeout 参数,我们为缓存响应指定了与 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 好
日期:2019 年 4 月 24 日星期三 17:41:34 GMT
内容类型:application/json
内容长度:612

$curl http://localhost/api/4/http/keyvals/access_tokens
{“tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA”:”{\”活动\”:true}”}

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

优化3:从内省响应中提取属性

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

具有令牌属性的令牌内省响应

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

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

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

从导出属性对代理请求的令牌内省响应

生产配置

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

  • 使用 NGINX(磁盘缓存)进行 OAuth 2.0 令牌自省
  • 使用 NGINX Plus 进行 OAuth 2.0 令牌自省(键值缓存)

摘要

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

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

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


评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注