flownative/openidconnect-client

Neos Flow 应用程序的 Open ID Connect (OIDC) 客户端实现

资助包维护!
robertlemke

安装数: 43,419

依赖项: 1

建议者: 0

安全: 0

星标: 6

关注者: 4

分支: 8

公开问题: 3

类型:neos-package


README

MIT license Packagist CI Maintenance level: Love

Flow 框架的 OpenID Connect 客户端

Flow 包提供了一个 OpenID Connect (OIDC) 客户端 SDK。OpenID Connect 是建立在 OAuth 2.0 之上的认证层。虽然 OAuth 旨在用于 授权,但 OpenID Connect 负责的是 认证 - 即验证人类或机器用户的身份,通常称为“实体”。

OIDC 通过一个 身份令牌 提供有关认证用户的信息,该令牌编码为安全的 JSON Web Token (JWT)。JWT 在客户端和服务器端应用程序中易于处理。ID 令牌中的数据通常是经过签名的,并且可以选择加密。

功能概述

此插件充当 Flow 认证提供者。它允许您使用浏览器和通过 API 与您的应用程序通信的机器用户(例如,其他应用程序)进行用户认证和授权。

本软件包的一些功能亮点:

  • 用于 Flow 应用程序或 Neos 网站的现有认证方法的即插即用替换
  • OIDC 自动发现支持,配置简单
  • 支持一个应用程序内的多个 OIDC 服务(服务器)
  • 基于 JWT 甜饼的 Flow 会话管理集成
  • 从声明映射 Flow 用户角色
  • 自动 JWT 签名验证
  • 通过承载访问令牌进行认证
  • 自动刷新过期的访问令牌
  • 通过 Flow 账户模型轻松访问 ID 令牌
  • 支持命令行进行测试

术语和背景

在部署 OpenID Connect 至您的应用程序之前,您应该熟悉相关概念。作为一个快速提醒,以下是您应该了解的一些术语。

认证与授权

认证是确认个人或其他实体身份的过程。用户将需要证明其身份 - 例如,通过提供用户名和密码。

授权是指验证实体允许执行哪些操作或可以访问哪些信息的过程。在这种情况下,它与身份无关,仅与权限有关。

在大多数情况下,您可能希望将这两个概念结合到您的应用程序中 - 但了解它们之间的区别很重要。

身份提供者

身份提供者负责为您处理认证和授权过程。流行的身份提供者包括 Google、Facebook、Microsoft、付费服务如 Auth0,或专门的设置如 gluu。身份提供者可能实现诸如用户名/密码认证或高级方法(如多因素认证)。

身份令牌

ID 令牌作为 JSON Web Token (JWT) 的一部分提供。它包含认证实体的身份数据。JWT 由标题、主体和签名组成。

声明

ID 令牌提供了有关实体(例如,用户)的信息。不同的信息片段可能是一个名称、指向个人资料图片的 URL 或电子邮件地址。这些信息片段被称为“声明”。由于它们是作为 JWT 的一部分签名的,因此您可以在不具体询问中央 API 的情况下信任它们。

承载访问令牌

访问令牌赋予持有人访问特定服务或其他资源的权限。这意味着,拥有此访问令牌的人被允许访问令牌所发行的资源。

访问令牌通常具有有限的生命周期,并且是为特定范围发行的。

受众

受众是你想要使用OpenId Connect进行保护的应用或服务。它是令牌的预期接收者,通常由一个地址标识,例如 https://my-application.example.com。然而,与XML命名空间一样,这个地址并不一定需要存在,它只是用作标识。

要求

为了使用此插件,你需要

  • 一个提供自动发现的OIDC身份提供者
  • 一个基于Flow 6.0或更高版本的应用(例如Neos)

安装

Flownative OpenID Connect插件通过Composer安装

composer require flownative/openidconnect-client

使用场景

以下是一些使用此插件示例

认证网络用户

例如,使用此插件对Neos后端的用户进行认证和授权。

你需要的是

  • 配置你的身份提供者的发现URI
  • 配置Flow使用此插件作为认证提供者
  • 配置HTTP链来管理JWT会话cookie
  • 配置Flow和此插件来设置用户角色

你将为此类型的应用使用 授权代码授予

认证应用程序

例如,使用此插件来认证和授权你的其他第三方服务访问你的应用程序提供的API。

  • 配置你的身份提供者的发现URI
  • 配置Flow使用此插件作为(第二个)认证提供者
  • 配置HTTP链来管理JWT会话cookie
  • 配置Flow和此插件来设置用户角色
  • 保护你的API方法并评估从身份令牌获得的进一步权限

你将为此类型的应用使用 客户端凭据授予

OpenID Connect发现

身份提供者通常在特定的URL(例如,https://id.example.com/.well-known/openid-configuration)上公开OIDC发现文档。Flownative OIDC客户端插件使用发现来配置授权、令牌和用户信息端点,JWKs位置,支持的范围等。

按照以下方式配置发现端点

Flownative:
  OpenIdConnect:
    Client:
      services:
        myService:
          options:
            discoveryUri: 'https://id.example.com/.well-known/openid-configuration'            

您可以通过从终端运行以下命令来检查发现是否正常工作

./flow oidc:discover myService

+---------------------------------------+-----------------------------------------------------------+
| Option                                | Value                                                     |
+---------------------------------------+-----------------------------------------------------------+
| issuer                                | https://id.example.com/                                   |
| authorization_endpoint                | https://id.example.com/authorize                          |
| token_endpoint                        | https://id.example.com/oauth/token                        |
| userinfo_endpoint                     | https://id.example.com/userinfo                           |
| mfa_challenge_endpoint                | https://id.example.com/mfa/challenge                      |
| jwks_uri                              | https://id.example.com/.well-known/jwks.json              |
| registration_endpoint                 | https://id.example.com/oidc/register                      |
| revocation_endpoint                   | https://id.example.com/oauth/revoke                       |
| scopes_supported                      | array (                                                   |
|                                       |   0 => 'openid',                                          |
|                                       |   1 => 'profile',                                         |
|                                       |   2 => 'offline_access',                                  |
|                                       |   3 => 'name',                                            |
|                                       |   4 => 'given_name',                                      |
|                                       |   5 => 'family_name',                                     |
|                                       |   6 => 'nickname',                                        |
|                                       |   7 => 'email',                                           |
|                                       |   8 => 'email_verified',                                  |
|                                       |   9 => 'picture',                                         |
|                                       |   10 => 'created_at',                                     |
|                                       |   11 => 'identities',                                     |
|                                       |   12 => 'phone',                                          |
|                                       |   13 => 'address',                                        |
|                                       | )                                                         |
| response_types_supported              | array (                                                   |
|                                       |   0 => 'code',                                            |
|                                       |   1 => 'token',                                           |
|                                       |   2 => 'id_token',                                        |
|                                       |   3 => 'code token',                                      |
|                                       |   4 => 'code id_token',                                   |
|                                       |   5 => 'token id_token',                                  |
|                                       |   6 => 'code token id_token',                             |
|                                       | )                                                         |
| code_challenge_methods_supported      | array (                                                   |
|                                       |   0 => 'S256',                                            |
|                                       |   1 => 'plain',                                           |
|                                       | )                                                         |
| response_modes_supported              | array (                                                   |
|                                       |   0 => 'query',                                           |
|                                       |   1 => 'fragment',                                        |
|                                       |   2 => 'form_post',                                       |

…

授权代码授予

授权代码授予用于通过网络浏览器认证用户。典型的应用流程如下

  1. 用户尝试访问受保护的页面(控制器操作)
  2. Flow检查用户是否有一个包含有效JWT的cookie
  3. 没有有效的JWT,因此重定向到身份提供者的登录页面
  4. 用户登录并被重定向回Flow
  5. 一个授权代码被传递给Flow,Flow使用它来在幕后获取访问令牌
  6. 从访问令牌中提取JWT并将其作为cookie发送到浏览器
  7. 在随后的网络请求中,浏览器发送cookie,Flow识别用户已被认证

Flow应用程序需要一个客户端标识符和一个客户端密钥,以便它可以从身份提供者请求授权代码。

以下是一个示例配置,它为Neos后端启用OIDC认证。请注意,这是一个原型。此集成需要进一步配置和定制实现才能达到生产就绪状态

Flownative:
  OpenIdConnect:
    Client:
      services:
        test:
          options:
            discoveryUri: 'https://id.example.com/.well-known/openid-configuration'
            clientId: 'abcdefghijklmnopqrstuvwxyz01234567890'
            clientSecret: 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5MA=='
      middleware:
        cookie:      
          # For testing purposes allow cookies without HTTPS:
          secure: false
          # Create an HTTP only cookie for increased security
          httpOnly: true

Neos:
  Flow:
    security:
      authentication:
        providers:
          # Re-use the Neos authentication provider so we automatically get the right
          # request patterns:
          'Neos.Neos:Backend':
            label: 'Neos Backend (OIDC)'
            provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider'
            providerOptions:
              audience: 'https://www.example.com/neos'
              roles: ['Neos.Neos:Administrator']
              accountIdentifierTokenValueName: 'sub'
              serviceName: 'test'
            token: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken'
            entryPoint: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectEntryPoint'
            entryPointOptions:
              serviceName: 'test'
              scopes: ['sub', 'profile', 'name']

        authenticationStrategy: atLeastOneToken

在不进行进一步编程的情况下,您需要手动创建一个与OIDC身份提供者提供的“sub”声明的用户名相同的Neos用户。

客户端凭据授予

客户端凭据授权比授权码授权简单一些,但只能用于受信任的第三方。因为您直接使用长期有效的客户端凭据(而不是通过交换访问令牌来换取授权码的额外步骤),所以不能在浏览器中使用这种授权类型,因为凭据在那里不会安全。

以下是一个包含两部分的示例:一个提供API的应用程序和另一个消耗该API的应用程序。

用作“听众”字符串的URI仅是按照惯例使用URI。实际上,它可以是任何其他字符串,但必须被您的身份提供者识别。

以下配置用于使用API的Flow应用程序中

Flownative:
  OpenIdConnect:
    Client:
      services:
        test:
          options:
            discoveryUri: 'https://id.example.com/.well-known/openid-configuration'
            clientId: 'abcdefghijklmnopqrstuvwxyz01234567890'
            clientSecret: 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5MA=='
            additionalParameters:
              audience: 'http://yourapp.localbeach.net/api/v1'

在您的应用程序中,您可能有一个服务类,该类封装了与API的通信。它可能看起来像这样(省略了一些代码以节省篇幅)

class BillingService
{
    public function sendAuthenticatedRequest(string $relativeUri, string $method = 'GET', array $bodyFields = []): ResponseInterface
    {
        $openIdConnectClient = new OpenIdConnectClient('test');

        $accessToken = $openIdConnectClient->getAccessToken(
            'test',
            $this->clientId,
            $this->clientSecret,
            '',
            Authorization::GRANT_CLIENT_CREDENTIALS,
            $this->additionalParameters
        );

        $httpClient = new Client(['allow_redirects' => false]);
        return $httpClient->request(
            $method,
            trim($this->apiBaseUri, '/') . '/' . $relativeUri,
            [
                'headers' =>
                    [
                        'Content-Type' => 'application/json',
                        'Authorization' => 'Bearer ' . $accessToken->getToken()
                    ],
                'body' => ($bodyFields !== [] ? \GuzzleHttp\json_encode($bodyFields) : '')
            ]
        );
    }
}

重要的是,您的代码使用OpenID Connect客户端检索访问令牌,然后将该令牌作为授权头的一部分发送到API。

提供API的应用程序需要以下配置,可能使用与消耗应用程序不同的客户端ID和密钥

Flownative:
  OpenIdConnect:
    Client:
      services:
        test:
          options:
            discoveryUri: 'https://id.example.com/.well-known/openid-configuration'
            clientId: 'abcdefghijklmnopqrstuvwxyz01234567890'
            clientSecret: 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXowMTIzNDU2Nzg5MA=='

在您的应用程序中提供API的基控制器可能看起来像这样

abstract class AbstractApiController extends ActionController
{
    /**
     * @Flow\Inject
     * @var Context
     */
    protected $securityContext;

    /**
     * @var IdentityToken|null
     */
    protected $identityToken;

    /**
     * @return void
     */
    public function initializeAction()
    {
        parent::initializeAction();

        $account = $this->securityContext->getAccount();
        $identityToken = $account->getCredentialsSource();
        if ($identityToken instanceof IdentityToken) {
            $this->identityToken = $identityToken;
        }
    }
}

来自身份令牌的职位

不是直接指定Flow认证角色,而是可以从身份令牌值中提取角色。令牌提供的角色必须与Flow策略配置中使用的相同标识符。

鉴于身份令牌提供了一个名为“https://flownative.com/roles”的声明,您可以按如下方式配置提供者

…
        security:
          authentication:
            providers:
              'Flownative.OpenIdConnect.Client:OidcProvider':
                label: 'OpenID Connect'
                provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider'
                providerOptions:
                  rolesFromClaims:
                    - 'https://flownative.com/roles'
                  …
 

当用户登录并且她的身份令牌具有包含Flow角色标识符数组的值“https://flownative.com/roles”时,OpenID Connect提供者将自动将这些角色分配给临时帐户。

如果其值不匹配所需的Flow角色模式(<Package-Key>:<Role>)或如果多个角色应转换为单个Flow角色,则可以将角色进行映射

…
    providerOptions:
      rolesFromClaims:
        -
          name: 'https://flownative.com/roles'
          mapping:
            'role1': 'Some.Package:SomeRole1'
            'role2': 'Some.Package:SomeOtherRole'
            'role3': 'Some.Package:SomeRole'
      …
 

您可以指定多个声明名称,这些名称都将用于编译角色列表。

如果事情没有按预期工作,请检查日志以获取提示。

来自现有账户的职位

作为第三个选项,可以使用与给定的身份令牌声明匹配的现有账户的角色。换句话说,如果有一个由身份令牌提供的相同用户名的账户,则可以使用该(持久)账户的角色。

鉴于身份令牌提供了一个名为“email”的声明,并且存在一个使用电子邮件地址作为其账户标识符的账户(例如,一个Neos用户账户),您可以按如下方式配置提供者

…
        security:
          authentication:
            providers:
              'Flownative.OpenIdConnect.Client:OidcProvider':
                label: 'OpenID Connect'
                provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider'
                providerOptions:
                  accountIdentifierTokenValueName: 'email'
                  addRolesFromExistingAccount: true
                  …
 

当用户登录并且她的身份令牌具有包含“alice@example.com”的值“email”时,OpenID Connect提供者将自动分配分配给具有相同账户标识符的任何Flow账户的角色。

您可以混合“rolesFromClaims”与“addRolesFromExistingAccount”。在这种情况下,声明和现有账户的角色将被合并。

再次,如果事情没有按预期工作,请检查日志以获取提示。

更多认证提供者选项

当您使用OpenID Connect认证提供者时,您可以提供额外的安全措施选项。

听众固定

建议指定您应用程序的“受众”标识符。这样,如果令牌的“aud”字符串与您应用程序中配置的字符串匹配,则您的身份提供者签发的令牌才会被接受进行身份验证。如果没有此配置,如果您的身份提供者的令牌具有有效的签名并且包含正确的角色声明,则您的应用程序将接受任何令牌。

…
        security:
          authentication:
            providers:
              'Flownative.OpenIdConnect.Client:OidcProvider':
                label: 'OpenID Connect'
                provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider'
                providerOptions:
                  audience: 'https://www.example.com/my-application'
                  …

密钥轮换和撤销

身份提供者用于签名JWT的密钥应定期轮换。此插件通过发现端点配置的JWKs端点检索有效密钥。

JWKs可能包含多个公钥。这样,现有的尚未过期的JWT仍然有效。

此插件支持多个密钥,因此无需进一步操作即可轮换密钥。但是,如果您在短时间内多次轮换密钥或撤销了现有密钥,您应刷新相应的缓存(或所有缓存)。

   ./flow flow:cache:flushone Flownative_OpenIdConnect_Client_JWKs

关于OpenID Connect的更多信息

另请参阅

https://openid.net/specs/openid-connect-basic-1_0.html https://connect2id.com/learn/openid-connect

Neos CMS的示例配置

使用以下配置,您可以使用OIDC对Neos CMS后端用户进行身份验证。OIDC仅用于身份验证(不用于授权)。为此,身份令牌必须通过“email”令牌值提供用户的电子邮件地址,并且必须存在具有此电子邮件地址(作为用户名)的Neos用户。

Flownative:
  OpenIdConnect:
    Client:
      services:
        neos:
          options:
            discoveryUri: '%env:OIDC_DISCOVERY_URI%'
            clientId: '%env:OIDC_CLIENT_ID%'
            clientSecret: '%env:OIDC_CLIENT_SECRET%'
            additionalParameters:
              audience: '%env:OIDC_AUDIENCE%'

      middleware:
        authenticationProviderName: 'Neos.Neos:Backend'

Neos:
  Flow:
    security:
      authentication:
        providers:
          'Neos.Neos:Backend':
            label: 'OpenID Connect'
            provider: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectProvider'

            requestPatterns:
              'Neos.Neos:BackendControllers':
                pattern: 'ControllerObjectName'
                patternOptions:
                  controllerObjectNamePattern: 'Neos\Neos\Controller\.*'
              'Neos.Neos:ServiceControllers':
                pattern: 'ControllerObjectName'
                patternOptions:
                  controllerObjectNamePattern: 'Neos\Neos\Service\.*'

            providerOptions:
              addRolesFromExistingAccount: true
              accountIdentifierTokenValueName: 'email'
              jwtCookieName: 'flownative_oidc_jwt'
              serviceName: 'neos'
            token: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectToken'
            entryPoint: 'Flownative\OpenIdConnect\Client\Authentication\OpenIdConnectEntryPoint'
            entryPointOptions:
              serviceName: 'neos'
              scope: 'profile email'

        authenticationStrategy: oneToken
    session:
      inactivityTimeout: 14400

致谢和支持

此库由Robert Lemke / Flownative开发。请随时在我们的GitHub项目提出新功能建议、报告错误或提供错误修复。

如果您想让我们开发新功能或在项目中实现OIDC方面需要帮助,请联系Robert