mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-28 18:14:35 -05:00
188 lines
6.3 KiB
PHP
188 lines
6.3 KiB
PHP
<?php
|
|
|
|
use App\Auth\Oidc\Exceptions\OidcSigningKeyNotFoundException;
|
|
use App\Auth\Oidc\Exceptions\OidcTokenException;
|
|
use App\Auth\Oidc\OidcDiscoveryDocument;
|
|
use App\Auth\Oidc\OidcTokenValidator;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class);
|
|
|
|
function oidc_base64url(string $value): string
|
|
{
|
|
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
|
}
|
|
|
|
function oidc_keyset(string $kid = 'test-key'): array
|
|
{
|
|
$privateKey = openssl_pkey_new([
|
|
'private_key_bits' => 2048,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
]);
|
|
|
|
openssl_pkey_export($privateKey, $privatePem);
|
|
$details = openssl_pkey_get_details($privateKey);
|
|
|
|
return [
|
|
'private_pem' => $privatePem,
|
|
'jwks' => [
|
|
'keys' => [[
|
|
'kty' => 'RSA',
|
|
'kid' => $kid,
|
|
'alg' => 'RS256',
|
|
'use' => 'sig',
|
|
'n' => oidc_base64url($details['rsa']['n']),
|
|
'e' => oidc_base64url($details['rsa']['e']),
|
|
]],
|
|
],
|
|
];
|
|
}
|
|
|
|
function oidc_token(array $claims, string $privatePem, string $kid = 'test-key', string $algorithm = 'RS256'): string
|
|
{
|
|
$header = oidc_base64url(json_encode(['alg' => $algorithm, 'typ' => 'JWT', 'kid' => $kid], JSON_THROW_ON_ERROR));
|
|
$payload = oidc_base64url(json_encode($claims, JSON_THROW_ON_ERROR));
|
|
$signatureInput = $header.'.'.$payload;
|
|
openssl_sign($signatureInput, $signature, $privatePem, OPENSSL_ALGO_SHA256);
|
|
|
|
return $signatureInput.'.'.oidc_base64url($signature);
|
|
}
|
|
|
|
function oidc_discovery(): OidcDiscoveryDocument
|
|
{
|
|
return new OidcDiscoveryDocument(
|
|
issuer: 'https://idp.example.com',
|
|
authorizationEndpoint: 'https://idp.example.com/oauth2/authorize',
|
|
tokenEndpoint: 'https://idp.example.com/oauth2/token',
|
|
userinfoEndpoint: 'https://idp.example.com/oauth2/userinfo',
|
|
jwksUri: 'https://idp.example.com/.well-known/jwks.json',
|
|
);
|
|
}
|
|
|
|
it('validates a well formed RS256 id token', function () {
|
|
$keyset = oidc_keyset();
|
|
$now = time();
|
|
$token = oidc_token([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => $now,
|
|
'exp' => $now + 600,
|
|
'nonce' => 'expected-nonce',
|
|
'email' => 'User@Example.com',
|
|
], $keyset['private_pem']);
|
|
|
|
$claims = app(OidcTokenValidator::class)->validate(
|
|
idToken: $token,
|
|
discovery: oidc_discovery(),
|
|
jwks: $keyset['jwks'],
|
|
clientId: 'client-id',
|
|
expectedNonce: 'expected-nonce',
|
|
);
|
|
|
|
expect($claims['sub'])->toBe('okta-user-1')
|
|
->and($claims['email'])->toBe('User@Example.com');
|
|
});
|
|
|
|
it('rejects invalid token claims', function (array $claimOverrides, string $message) {
|
|
$keyset = oidc_keyset();
|
|
$now = time();
|
|
$claims = array_merge([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => $now,
|
|
'exp' => $now + 600,
|
|
'nonce' => 'expected-nonce',
|
|
], $claimOverrides);
|
|
|
|
$token = oidc_token($claims, $keyset['private_pem']);
|
|
|
|
app(OidcTokenValidator::class)->validate(
|
|
idToken: $token,
|
|
discovery: oidc_discovery(),
|
|
jwks: $keyset['jwks'],
|
|
clientId: 'client-id',
|
|
expectedNonce: 'expected-nonce',
|
|
);
|
|
})->throws(OidcTokenException::class)->with([
|
|
'issuer mismatch' => [['iss' => 'https://evil.example.com'], 'issuer'],
|
|
'audience mismatch' => [['aud' => 'other-client'], 'audience'],
|
|
'azp missing for multi audience' => [['aud' => ['client-id', 'other-client']], 'azp'],
|
|
'azp mismatch' => [['aud' => ['client-id', 'other-client'], 'azp' => 'other-client'], 'azp'],
|
|
'expired token' => [['exp' => time() - 3600], 'expired'],
|
|
'future issued at' => [['iat' => time() + 3600], 'issued'],
|
|
'nonce mismatch' => [['nonce' => 'wrong-nonce'], 'nonce'],
|
|
'missing subject' => [['sub' => null], 'subject'],
|
|
'empty subject' => [['sub' => ''], 'subject'],
|
|
'non-string subject' => [['sub' => 123], 'subject'],
|
|
]);
|
|
|
|
it('rejects a bad signature and unknown key id', function (string $kid) {
|
|
$keyset = oidc_keyset('test-key');
|
|
$otherKeyset = oidc_keyset($kid);
|
|
$now = time();
|
|
$token = oidc_token([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => $now,
|
|
'exp' => $now + 600,
|
|
'nonce' => 'expected-nonce',
|
|
], $otherKeyset['private_pem'], $kid);
|
|
|
|
app(OidcTokenValidator::class)->validate(
|
|
idToken: $token,
|
|
discovery: oidc_discovery(),
|
|
jwks: $keyset['jwks'],
|
|
clientId: 'client-id',
|
|
expectedNonce: 'expected-nonce',
|
|
);
|
|
})->throws(OidcTokenException::class)->with([
|
|
'same kid with bad signature' => ['test-key'],
|
|
'unknown kid' => ['other-key'],
|
|
]);
|
|
|
|
it('rejects disallowed algorithms', function () {
|
|
$keyset = oidc_keyset();
|
|
$now = time();
|
|
$token = oidc_token([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => $now,
|
|
'exp' => $now + 600,
|
|
], $keyset['private_pem'], algorithm: 'HS256');
|
|
|
|
app(OidcTokenValidator::class)->validate($token, oidc_discovery(), $keyset['jwks'], 'client-id');
|
|
})->throws(OidcTokenException::class);
|
|
|
|
it('throws a dedicated exception when the signing key is unknown', function () {
|
|
$keyset = oidc_keyset('current-key');
|
|
$token = oidc_token([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => time(),
|
|
'exp' => time() + 600,
|
|
], $keyset['private_pem'], 'rotated-key');
|
|
|
|
app(OidcTokenValidator::class)->validate($token, oidc_discovery(), $keyset['jwks'], 'client-id');
|
|
})->throws(OidcSigningKeyNotFoundException::class);
|
|
|
|
it('rejects a jwks key not designated for signing', function () {
|
|
$keyset = oidc_keyset();
|
|
$keyset['jwks']['keys'][0]['use'] = 'enc';
|
|
$now = time();
|
|
$token = oidc_token([
|
|
'iss' => 'https://idp.example.com',
|
|
'aud' => 'client-id',
|
|
'sub' => 'okta-user-1',
|
|
'iat' => $now,
|
|
'exp' => $now + 600,
|
|
], $keyset['private_pem']);
|
|
|
|
// An encryption-only key is dropped from the keyset, so the kid no longer resolves.
|
|
app(OidcTokenValidator::class)->validate($token, oidc_discovery(), $keyset['jwks'], 'client-id');
|
|
})->throws(OidcTokenException::class);
|