ingenerator / oidc-token-verifier
一个轻量级的库,用于验证OIDC令牌与公共发现文档/JWKS集合
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0
- ext-json: *
- ext-openssl: *
- firebase/php-jwt: ^6.0
- guzzlehttp/guzzle: ^7.0
- psr/cache: ^1.1 || ^2.0 || ^3.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- fig/log-test: ^1.1
- phpunit/phpunit: ^9.5.5
README
oidc-token-verifier是一个轻量级的PHP验证器,用于验证OIDC ID令牌,该令牌用于OpenID Connect协议。
$> 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 许可证 下授权