mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-27 17:55:59 -04:00
fix(auth): refresh OIDC JWKS and block unverified account linking
This commit is contained in:
@@ -93,6 +93,21 @@ it('creates a new oidc user when provider registration is allowed while normal r
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects linking an unverified oidc email to an existing local account', function () {
|
||||
$user = User::factory()->create(['email' => 'victim@example.com']);
|
||||
|
||||
fakeOidcProvider(['email' => 'victim@example.com', 'email_verified' => false]);
|
||||
|
||||
$response = $this->from('/login')->get(route('auth.callback', 'oidc'));
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
$this->assertGuest();
|
||||
$this->assertDatabaseMissing('oauth_identities', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'oidc',
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects new oidc users when neither normal nor provider registration is enabled', function () {
|
||||
fakeOidcProvider(['email' => 'blocked@example.com']);
|
||||
|
||||
|
||||
@@ -38,6 +38,25 @@ it('fetches and caches discovery documents and jwks', function () {
|
||||
Http::assertSentCount(2);
|
||||
});
|
||||
|
||||
it('refetches jwks once on forced refresh to pick up rotated keys', function () {
|
||||
Cache::flush();
|
||||
Http::fakeSequence('https://idp.example.com/jwks')
|
||||
->push(['keys' => [['kid' => 'old']]])
|
||||
->push(['keys' => [['kid' => 'new']]]);
|
||||
|
||||
$service = app(OidcDiscoveryService::class);
|
||||
|
||||
expect($service->jwks('https://idp.example.com/jwks')['keys'][0]['kid'])->toBe('old');
|
||||
|
||||
// Forced refresh bypasses the cache and sees the rotated key.
|
||||
expect($service->jwks('https://idp.example.com/jwks', true)['keys'][0]['kid'])->toBe('new');
|
||||
Http::assertSentCount(2);
|
||||
|
||||
// Cooldown prevents a second immediate upstream fetch; cached value returned.
|
||||
expect($service->jwks('https://idp.example.com/jwks', true)['keys'][0]['kid'])->toBe('new');
|
||||
Http::assertSentCount(2);
|
||||
});
|
||||
|
||||
it('rejects invalid discovery and jwks payloads', function () {
|
||||
Cache::flush();
|
||||
Http::fake([
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Auth\Oidc\Exceptions\OidcSigningKeyNotFoundException;
|
||||
use App\Auth\Oidc\Exceptions\OidcTokenException;
|
||||
use App\Auth\Oidc\OidcDiscoveryDocument;
|
||||
use App\Auth\Oidc\OidcTokenValidator;
|
||||
@@ -154,3 +155,32 @@ it('rejects disallowed algorithms', function () {
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user