npr/npr-one-backend-proxy

用于与NPR One API授权服务器交互的服务器端代理

v4.3.4 2023-05-03 17:45 UTC

README

基于PHP的服务器端代理,用于与NPR One API授权服务器交互。使用此代理来保护您的OAuth2凭据。

Packagist Packagist Packagist Build Status Coverage Status

目录

背景

NPR One API提供了一个轻量级的REST/超媒体接口,以支持NPR One体验。为了保护我们的API,我们实现了一个基于OAuth 2.0协议的授权服务器,这是一个广泛接受的互联网标准。

第三方开发者有两种主要方法可以获得我们的API所需的访问令牌,以与我们的任何其他微服务交互

由于安全顾虑,NPR One授权服务器目前不接受OAuth2规范中描述的implicit授权类型。

device_codeauthorization_code授权类型都需要OAuth2client_secret来生成访问令牌。然而,由于客户端语言(如JavaScript)编写的Web应用程序的源代码无法保密,因此需要服务器端代理来安全地调用授权服务器,并确保OAuth2凭据的安全性。

为了让第三方开发者更容易接受这一要求,我们提供此PHP代理作为开源软件包,以帮助您快速启动并运行,并防止NPR One客户端凭据在公共源代码中泄露。

设置

此项目旨在在Apache HTTP服务器Nginx服务器环境中执行。

预先条件

需要PHP的最近版本,版本等于或大于7.3.0。

此包中提供的默认EncryptionProvider类依赖于OpenSSL扩展。如果不可用OpenSSL,消费者可以选择实现一个自定义的EncryptionProvider类,该类实现我们的EncryptionInterface。(有关更多信息,请参阅EncryptionProvider部分。)

使用NPR的授权服务器需要注册的开发者账号,并具有NPR One开发者中心的账户。如果您还没有开发者中心账户,可以注册个人账户并立即开始。

安装

本项目旨在作为一个子模块(或依赖项)运行于更大的项目之中,应使用Composer(PHP项目的开源依赖管理器)进行安装。

[sudo] composer install npr/npr-one-backend-proxy

如果您还没有设置Composer项目,可以使用以下命令快速开始:

composer init --require="npr/npr-one-backend-proxy" -n

集成

要将此包集成到您的项目中,必须创建以下2个PHP类:

此外,如果您使用的是authorization_code授权,则需要一个StorageProvider类。每个类的示例可以在示例文件夹中找到。

必需的类

路由器

创建一个路由器,该路由器调用AuthCodeControllerDeviceCodeController中相关的公共方法,具体取决于将使用哪种授权类型(分别是authorization_codedevice_code)。

所有消费者,无论授权类型如何,都必须实现一个路由,该路由映射到RefreshTokenController类中的generateNewAccessTokenFromRefreshToken()函数。此路由允许您的服务器在原始访问令牌过期时无缝请求新的访问令牌。如果您不实现此路由,您的用户将在两周后自动注销,并需要重新登录才能继续收听,这不是期望的用户体验。

同样,所有消费者(假设它们提供某种“注销”或“从NPR One断开连接”的功能)应实现一个路由,该路由映射到LogoutController类中的deleteAccessAndRefreshTokens()函数。此路由允许您的应用确保与登录状态相关的所有持久数据(如访问令牌和刷新令牌)从NPR的授权服务器中删除,同时确保从安全存储层中删除refresh_token。此函数接受一个访问令牌,但也可以在没有输入的情况下工作,前提是刷新令牌被持久存储在安全存储层中。

examples/Router.php文件提供了一个假设的Laravel风格示例,说明这可能是什么样子。请注意,此代码仅作为示例,旨在提供指导,尚未经过测试。此示例包括用于authorization_codedevice_code授权类型的代码,但在您的实际实现中,仅包含与您应用程序中使用的授权类型相关的代码。

配置提供者

创建一个实现我们的ConfigInterface的ConfigProvider类,以为您的控制器类提供动力。ConfigProvider类将封装用于驱动此OAuth2代理的消费者特定变量(您的客户端ID和客户端密钥)。

examples 文件夹中有一个示例 ConfigProvider.php 文件,可以帮助您开始使用。这个类不需要太复杂,大部分情况下只需返回硬编码的字符串。然而,请不要将您的客户端密钥(或您的加密盐)包含在任何将出现在公共仓库中的文件中,因为这可能会危害您的应用程序。我们假设您要么打算保持您的代码私有,要么您有一些私有密钥文件,这些文件不包括在任何公共仓库或公开访问的位置。

条件性必需

存储提供者

如果您使用的是 authorization_code 授权(以及 AuthCodeController),则需要创建一个实现我们 StorageInterface 的 StorageProvider 类。StorageProvider 需要验证 OAuth2 的 state 参数。

您将在 examples 文件夹中找到一个示例 StorageProvider.php 文件。示例使用了 Predis,这是一个 PHP Redis 客户端,但还有许多其他选择,包括 MemcachedPHP sessions。MySQL 也是一个选项,但不推荐使用,因为它可能要慢得多。我们选择 Predis 进行演示,因为其语法非常简单,适用于许多其他存储层。

可选

加密提供者

默认情况下,Controller 类将通过 Cookie 保存刷新令牌和访问令牌。为了保持这些刷新令牌的安全,我们在保存之前加密它们,在需要检索它们时解密它们。为了使这个过程更简单,提供了一个默认的 EncryptionProvider。然而,这个特定的 EncryptionProvider 依赖于可用的 OpenSSL 扩展,这可能不是所有开发者的选择。如果 OpenSSL 不可用,或者您想使用不同的加密方法,您可以使用一个自定义的加密提供程序,该程序实现了我们的 EncryptionInterface

如果您选择实现自定义加密提供程序,请使用 默认实现 作为您的示例。包含您自己的自定义加密提供程序的语法如下

authorization_code 授权类型

use NPR\One\Controllers\AuthCodeController;
use Your\Package\Here\ConfigProvider;
use Your\Package\Here\EncryptionProvider;
use Your\Package\Here\StorageProvider;

$controller = (new AuthCodeController())
        ->setConfigProvider(new ConfigProvider())
        ->setStorageProvider(new StorageProvider())
        ->setEncryptionProvider(new EncryptionProvider());

device_code 授权类型

use NPR\One\Controllers\DeviceCodeController;
use Your\Package\Here\ConfigProvider;
use Your\Package\Here\EncryptionProvider;

$controller = (new DeviceCodeController())
        ->setConfigProvider(new ConfigProvider())
        ->setEncryptionProvider(new EncryptionProvider());
安全存储提供者

如上所述,加密的 Cookie 用于在会话之间存储刷新令牌。然而,Cookie 不是唯一的存储方法:RedisMemcached 是很好的选择(只要您有机制在会话之间识别用户,这可能仍然需要使用 Cookie)。如果您考虑使用 PHP 的会话存储,您可能想看看 PHP-Secure-Session,它通过加密提供额外的安全层。

所有控制器类都配置为使用默认的安全存储层 SecureCookieProvider,但您可以使用 setSecureStorageProvider() 函数轻松覆盖。

authorization_code 授权类型

use NPR\One\Controllers\AuthCodeController;
use Your\Package\Here\ConfigProvider;
use Your\Package\Here\SecureStorageProvider;
use Your\Package\Here\StorageProvider;

$controller = (new AuthCodeController())
        ->setConfigProvider(new ConfigProvider())
        ->setStorageProvider(new StorageProvider())
        ->setSecureStorageProvider(new SecureStorageProvider());

device_code 授权类型

use NPR\One\Controllers\DeviceCodeController;
use Your\Package\Here\ConfigProvider;
use Your\Package\Here\SecureStorageProvider;

$controller = (new DeviceCodeController())
        ->setConfigProvider(new ConfigProvider())
        ->setSecureStorageProvider(new SecureStorageProvider());

您的自定义安全存储提供者类需要实现 StorageInterface 接口,但除此之外没有特殊要求。如果您使用 Redis 或 Memcached 等工具,则不需要加密或解密令牌,因为这些系统通常已经是隐式安全的。只有 SecureCookieProvider 类才明确需要加密。

实现细节

继续阅读以了解此包如何在幕后操作,这将帮助指导您的客户端应用程序如何与此后端代理交互。

授权代码授予

authorization_code 流有两个阶段,在我们这里对应于 AuthCodeController 类中的 startAuthorizationGrant()completeAuthorizationGrant() 函数。

  • 第一阶段: startAuthorizationGrant() 构造调用所需的查询参数,并将它们附加到 https://authorization.api.npr.org/v2/authorize。然后,您的路由器应将浏览器重定向到该 URL(无论是使用框架的内置功能,如 Laravel 的 redirect()->away($url),还是使用传统的 header("Location: $url"))。

  • 第二阶段: completeAuthorizationGrant() 应映射到您在 NPR One 开发者控制台 中为客户端应用程序添加的 redirect_uri。此函数有两个主要职责

    1. 验证在 startAuthorizationGrant() 阶段生成的 state 参数。这项额外检查确保您的调用没有被恶意第三方拦截。
    2. 使用 POST https://authorization.api.npr.org/v2/token 端点将授权代码交换为实际访问令牌。

然后使用我们的 CookieProvider 类将令牌保存到未加密的名为 access_token 的 cookie 中。**注意**:**强烈**建议您的客户端应用程序检索 cookie 的值,将其存储在本地(HTML5 localStorage 是一个不错的选择),然后**删除** cookie。否则,由于 cookie 没有加密,它不被认为是安全的,并且也可能导致后续 HTTP 请求的额外开销。

请注意,completeAuthorizationGrant() 函数确实返回了一个 AccessTokenModel,但由于 authorization_code 授权旨在通过重定向浏览器来工作,因此不建议您实际从这个端点返回 JSON。相反,您将希望使用 getRedirectUri() 函数返回到您的客户端应用程序,然后根据上述描述从 cookie 中检索访问令牌。

设备代码授予

device_code 授权同样有两个阶段,但客户端需要做更多的工作。DeviceCodeController 类有两个公共方法:startDeviceCodeGrant()pollDeviceCodeGrant();每个都应该映射到您的路由器中的一个唯一端点。

  • 第一阶段:客户端通过调用对应于 startDeviceCodeGrant() 函数的路由开始此过程,该函数调用 POST https://authorization.api.npr.org/v2/device 端点,然后执行两件事:一是安全地存储 device_code(值),无论是通过加密cookie还是使用如此处所述的自定义安全存储提供者;二是将 所有其他内容 作为JSON对象返回给消费者。然后,消费者负责在屏幕上显示 user_codeverification_uri

  • 第二阶段:接下来,客户端负责对对应于 pollDeviceCodeGrant() 函数的路由进行 轮询,该函数使用安全存储的 device_code 调用 POST https://authorization.api.npr.org/v2/token 端点,并检查用户是否已登录(如果已登录,则返回访问令牌,如果没有登录,则抛出异常)。这种轮询应不超过前一个调用返回的JSON对象中的 interval 值。

所有设备代码/用户代码对都将在前一个调用返回的JSON对象中的 expires_in 值内过期(此值表示秒数)。如果用户在设备代码过期之前未能登录,客户端应用程序负责调用对应于 startDeviceCodeGrant() 函数的路由来重新启动此过程。

刷新令牌授予

与每个新访问令牌相关联的 refresh_token 应安全地存储在加密cookie中或使用如此处所述的自定义安全存储提供者。因此,RefreshTokenController 类非常简单,只有一个方法

  • generateNewAccessTokenFromRefreshToken() 在安全存储提供者中查找此 refresh_token(如果找到),并使用 POST https://authorization.api.npr.org/v2/token 端点提供的 refresh_token 授予来为用户获取新的访问令牌。(顺便说一下:是的,这个调用将导致生成新的 refresh_token,然后以完全相同的方式保存到安全存储层。)

当任何之前获取了有效访问令牌的客户端应用程序突然从我们的任何微服务收到 401 未授权 响应时,应调用此方法,指示访问令牌已过期。此错误应调用调用 generateNewAccessTokenFromRefreshToken() 的您的路由器中的端点。将生成新的访问令牌并以原始JSON格式返回(此时,客户端应用程序负责将其安全存储)。如果无法生成新的访问令牌,客户端可以重试调用2-3次,但在此之后,应认为用户已注销并提示其再次登录。

可选实现: AccessTokenModel 和相应的JSON输出确实包括访问令牌的 expires_in 值(秒数TTL),因此客户端应用程序可以在令牌实际过期之前或之后将其设置为过期但在尝试另一个API调用之前调用对应于 generateNewAccessTokenFromRefreshToken() 的路由(但不是必须的)。请注意,无论它是否已经过期,原始访问令牌都将作为该调用的一部分立即被删除。

注销/断开连接

我们要求所有客户通过实施一种注销功能来帮助确保用户数据安全并释放系统中的未使用资源。该功能将通过 POST https://authorization.api.npr.org/v2/token/revoke 端点撤销用户之前生成的访问令牌和刷新令牌。在 LogoutController 类中的 deleteAccessAndRefreshTokens() 函数将执行此任务,同时删除之前保存到加密cookie或您自定义的 安全存储提供程序 中的 refresh_token。您的客户端应用程序可以忽略您用于安全存储刷新令牌的任何机制,并安全地假设它作为注销过程的一部分被正确删除。

NPR One API 参考文档 所述,POST https://authorization.api.npr.org/v2/token/revoke 端点接受访问令牌或刷新令牌。默认情况下,它假定是访问令牌,但它将删除 两者,无论传递的是哪一个。因此,deleteAccessAndRefreshTokens() 函数 可以 接受访问令牌,但如果未提供,它将寻找刷新令牌,如果找到,则使用该令牌撤销这对令牌。如果您有访问令牌(特别是对于在2016年夏季之前开发的客户端应用程序,那时首次引入刷新令牌),建议您传递访问令牌。如果您确信已为所有用户颁发刷新令牌,并且没有其他客户端代码删除它们的风险,则可以安全地不带任何参数调用 deleteAccessAndRefreshTokens()

此代理不对您如何设置和调用端点提出任何要求(仅限于OAuth 2.0规范所严格要求的),因此 deleteAccessAndRefreshTokens() 函数所需的访问令牌可以从各种来源获取:通过查询参数、表单 POST 数据、带有JSON主体的 POST,甚至可能是cookie,如果这是您在客户端存储访问令牌的方式。示例 Router.php 文件出于简单起见使用了查询参数。在大多数情况下,带有表单数据或JSON主体的 POST 请求更可取,因为它们在非安全网络中更难以拦截,但由于假设访问令牌几乎立即被撤销,因此保持令牌安全不是一个大问题。

文档

有关此包的公共API的更多信息,请参阅 docs 文件夹。

有关NPR One API和我们的OAuth2使用的背景信息,请参阅 开发者指南,网址为 NPR One 开发者中心。特别是,关于 授权服务 的部分可能很有趣。

贡献

如果您有兴趣通过提交错误报告、帮助改进文档或编写实际代码来为此项目做出贡献,请阅读 我们的贡献指南

许可证

版权所有(c)2016 NPR

根据以下修改后的条款,在 Apache License, Version 2.0(以下简称“许可证”)下授予许可;您只能在不违反许可证的修改条款的情况下使用此文件;增加第10节,内容如下

10. 附加禁止条款

在使用作品时,您不得(或允许代表您行事的人)

a. 以引入病毒、蠕虫、缺陷、特洛伊木马、恶意软件或其他具有破坏性或恶意性质的物品到作品、NPR One API、NPR服务器或网络基础设施,或任何NPR产品和服务为目的执行任何行为;或者未经授权访问NPR One API、NPR服务器或网络基础设施,或任何NPR产品或服务;

b. 删除、遮挡或修改任何NPR服务条款,包括NPR服务使用条款开发者API使用条款,或指向或通知这些条款的链接;或者

c. 执行任何NPR服务条款禁止的其他行为,包括NPR服务使用条款开发者API使用条款

您可以在https://apache.ac.cn/licenses/LICENSE-2.0获取许可证副本。

除非适用法律要求或书面同意,否则在许可证下修改的软件以“现状”为基础分发,不提供任何形式的保证或条件,无论是明示的还是暗示的。请参阅许可证以了解具体规定许可证下权限和限制的语言。