ingenerator/oidc-token-verifier

一个轻量级的库,用于验证OIDC令牌与公共发现文档/JWKS集合

v1.1.0 2023-11-06 13:04 UTC

This package is auto-updated.

Last update: 2024-09-06 14:38:39 UTC


README

oidc-token-verifier是一个轻量级的PHP验证器,用于验证OIDC ID令牌,该令牌用于OpenID Connect协议。

Tests

$> composer require ingenerator/oidc-token-verifier

OIDC令牌的使用

在完整的OpenID Connect规范中,ID令牌是多步最终用户授权流程的一部分。这与使用OAuth基于第三方身份验证提供者验证用户的概念类似。

然而,OIDC令牌也可以用于轻量级的服务器到服务器的身份验证。例如,它们可以用来授权Google Cloud Tasks的HTTP请求。

这种类型的服务器到服务器流程不需要完整的OpenID Connect协议。它只需要验证ID令牌本身的能力。ID令牌是一个JWT,由发行者进行加密签名。验证令牌涉及验证签名与发行者的公钥,这些公钥可以从一个知名的HTTP端点获取,然后对令牌的内容进行一些检查。

尽管存在许多PHP JWT库,但我们很难找到支持证书发现/声明断言阶段的库。这个库填补了这一空白。

请注意,所有加密/JWT级别的操作都委托给了firebase/php-jwt包。另外,请注意,目前我们仅支持RSA密钥和RS256令牌算法。

使用

您可以使用OIDCTokenVerifier验证令牌。为了单元测试,还有一个使用相同接口的MockTokenVerifier

在简单的情况下,您会这样做

// Where the AUTHORIZATION header is `Bearer {token}`
use Google\Auth\Cache\SysVCacheItemPool;use Ingenerator\OIDCTokenVerifier\OIDCTokenVerifier;use Ingenerator\OIDCTokenVerifier\OpenIDDiscoveryCertificateProvider;use Ingenerator\OIDCTokenVerifier\TokenConstraints;use Ingenerator\OIDCTokenVerifier\TokenVerificationResult;use Psr\Log\NullLogger;use test\mock\Ingenerator\OIDCTokenVerifier\Cache\MockCacheItemPool;
[$bearer, $jwt] = explode(' ', $_SERVER['HTTP_AUTHORIZATION']);
$verifier = new OIDCTokenVerifier(
    new OpenIDDiscoveryCertificateProvider(
        new \GuzzleHttp\Client, 
        // Any psr-6 CacheItemPoolInterface implementation, used for caching issuer certificates
        new CacheItemPoolInterface,  
        new NullLogger // Any PSR logger
    ),
    // You *must* explicitly provide the issuer your application expects to receive tokens from.
    // The verifier will *only* request certificates from this issuer. Otherwise, any third party could set up an HTTP 
    // certificate endpoint and send you tokens signed by them.
    //
    // If your application may receive tokens from more than one issuer, you will need to (securely) identify the issuer
    // of a specific token and then create an appropriate verifier.
    // 
    'https://#'
);

// See below for details of the TokenConstraints argument
$result = $verifier->verify($jwt, TokenConstraints::signatureCheckOnly()); 

// You can either interrogate the result like this
if ( ! $result->isVerified()) {
    echo "NOT AUTHORISED\n";
    echo $result->getFailure()->getMessage()."\n";
} else {
    // The JWT payload is available from the result object
    echo "Authorised as ".$result->getPayload()->email."\n";
}

// Or if you'd prefer to throw an exception on failed auth this will:
// - Throw TokenVerificationFailedException if verification failed
// - Return the verified result if successful
$result = TokenVerificationResult::enforce($result);

额外约束

默认情况下,库只执行基本的JWT验证 - 签名、过期时间/之前时间等。

出于安全考虑,通常需要额外的验证。例如,任何Google Cloud Platform用户都可以生成由https://#签名的有效JWT,因此您通常希望根据audience(令牌是为谁创建的)和email(用于创建它的服务帐户)进行授权。

库提供对以下常见约束的原生支持

$verifier->verify($jwt, new TokenConstraints([
    // The audience (`aud` claim) of the JWT must exactly match this value
    // Some google services use the URL that is being called. Others provide a custom value - an app/client ID, etc
    'audience_exact' => 'https://my.app.com/task-handler-url',
    
    // The audience (`aud` claim) of the JWT is a URL and the path (and querystring if any) must match this value
    // In some loadbalanced environments it's hard to detect the external protocol or hostname from an incoming
    // request - e.g. a request to https://my.app.loadbalancer may appear to PHP as being to http://app.cluster.local.
    // Although this can be worked round with custom headers (X_FORWARDED_PROTO etc) these introduce other risks and
    // ultimately couple the app implementation to architectural concerns. In many cases, it's enough to verify the
    // the resource the token was generated for (path and querystring) without caring about scheme and hostname. This
    // alone prevents using a stolen token to perform a different operation. Cross-environment / cross-site attacks
    // are instead protected by using different service accounts for each separate logical system so that e.g a token
    // generated for QA cannot ever authorise that operation in production regardless of the hostnames used.
    'audience_path_and_query' => 'http://appserver.internal/action?record_id=15',

    // The JWT must contain an `email` claim, and it must exactly match this value
    'email_exact' => 'my-service-account@myproject.serviceaccount.test',
    
    // The JWT must contain an `email` claim, and it must exactly match one of these values
    // Useful when you have a short list of service accounts that may be allowed to call your endpoint    
    'email_exact' => [
        'my-service-account@myproject.serviceaccount.test',
        'my-service-account@myotherproject.serviceaccount.test',
    ],
    
    // The JWT must contain an `email` claim, and it must match this regex
    // Useful when you want to e.g. authorize all service accounts in a particular domain - use with caution!
    'email_match' => '/@myproject.serviceaccount.test$/'  
]));

您可以轻松地支持额外的自定义约束,例如验证额外的自定义声明

class MyTokenConstraints extends TokenConstraints {
    
    protected static function getAllMatchers(): array {
        $matchers = parent::getAllMatchers();
        // Constraint matchers are an array of {name} => boolean function indicating if the payload matches
        $matchers['user_role_contains'] = function (\stdClass $payload, string $expect) {
            // $payload is the decoded JWT
            // We check it has a custom claim ->user_roles as an array of roles
            return in_array($expect, $payload->user_roles ?? [], TRUE);       
        };
        return $matchers;    
    }
}

$verifier->verify($jwt, new MyTokenConstraints([
    'audience_exact'     => 'https://foo.bar/something',
    'user_role_contains' => 'administrator'
]));

如果您的应用程序分别处理授权(或用于测试目的),您可以使用TokenConstraints::signatureCheckOnly()方法创建一个空的约束集。

证书发现和缓存

默认情况下,库使用OpenIDDicoveryCertificateProvider动态获取给定发行者的公共证书。这使用{issuer}/.well-known/openid-configuration发现文档来查找发行者的JWKS URL。然后从JWKS URL获取证书,解码并缓存(在PSR-6缓存中)以供后续请求使用。

由于显而易见的原因,发现文档和JWKS 必须通过HTTPS提供。在开发环境中,例如,如果与模拟器一起工作,您可能没有HTTPS可用。在这种情况下,传递allow_insecure => TRUE选项以启用通过HTTP获取证书。

缓存生命周期基于 JWKS 响应的 Expires 头部。请注意,我们不会缓存(或关注)OpenID Discovery Document 本身的缓存头部。如果颁发者更改其 jwks_uri,则直到 JWKS 响应本身过期才会检测到。

在检索或刷新证书时,偶尔可能会发生网络/颁发者错误。由于 JWKS 变化相对较少,默认行为是记录失败,但可以使用过时的缓存值长达 2 小时。这可以通过 OpenIDDiscoveryCertificateProvider 的 cache_refresh_grace_period 选项进行配置。

基于 HTTP 的发现是最简单且推荐的方法,因为它允许颁发者控制的证书和密钥轮换。但是,如果您希望使用硬编码/替代颁发者证书源,则可使用 ArrayCertificateProvider(或您自己的实现)。

贡献

欢迎贡献,但在开始实质性工作之前,请与我们联系(例如,通过提交问题):我们可能有与您不同的特定要求/观点。

贡献者

本软件包由 inGenerator Ltd 赞助

  • Andrew Coulton acoulton - 主开发人员

许可证

BSD-3-Clause 许可证 下授权