b14bd302a3
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
284 lines
8.7 KiB
PHP
284 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Unit\Security;
|
|
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Models\Tenant\TenantConfiguration;
|
|
use KTXC\Resource\ProviderManager;
|
|
use KTXC\Security\Authentication\AuthenticationRequest;
|
|
use KTXC\Security\AuthenticationManager;
|
|
use KTXC\Security\Event\AuthenticationFailedEvent;
|
|
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
|
use KTXC\Service\TokenService;
|
|
use KTXC\Service\UserAccountsService;
|
|
use KTXF\Cache\CacheScope;
|
|
use KTXF\Cache\EphemeralCacheInterface;
|
|
use KTXF\Event\EventDispatcherInterface;
|
|
use KTXF\Security\Authentication\AuthenticationProviderInterface;
|
|
use KTXF\Security\Authentication\AuthenticationSession;
|
|
use KTXF\Security\Authentication\ProviderContext;
|
|
use KTXF\Security\Authentication\ProviderResult;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class AuthenticationManagerTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function completedAuthenticationPublishesAuthenticationSucceededEvent(): void
|
|
{
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('identifier')->willReturn('tenant-a');
|
|
$tenant->method('configuration')->willReturn(new TenantConfiguration());
|
|
|
|
$session = new AuthenticationSession(
|
|
id: 'auth-session',
|
|
state: AuthenticationSession::STATE_IDENTIFIED,
|
|
tenantIdentifier: 'tenant-a',
|
|
userIdentifier: 'user-a',
|
|
userIdentity: 'person@example.com',
|
|
methodsAvailable: ['password'],
|
|
createdAt: time(),
|
|
expiresAt: time() + 300,
|
|
);
|
|
|
|
$cache = $this->createMock(EphemeralCacheInterface::class);
|
|
$cache->expects($this->once())
|
|
->method('get')
|
|
->with('auth-session', CacheScope::Tenant, 'auth')
|
|
->willReturn($session);
|
|
$cache->expects($this->once())->method('set')->willReturn(true);
|
|
$cache->expects($this->once())
|
|
->method('delete')
|
|
->with('auth-session', CacheScope::Tenant, 'auth')
|
|
->willReturn(true);
|
|
|
|
$provider = new SuccessfulAuthenticationProvider();
|
|
$providers = $this->createStub(ProviderManager::class);
|
|
$providers->method('resolve')->willReturn($provider);
|
|
|
|
$users = $this->createMock(UserAccountsService::class);
|
|
$users->expects($this->once())
|
|
->method('fetchByIdentifier')
|
|
->with('user-a')
|
|
->willReturn([
|
|
'uid' => 'user-a',
|
|
'identity' => 'person@example.com',
|
|
'label' => 'Person',
|
|
'permissions' => [],
|
|
]);
|
|
|
|
$tokens = $this->createStub(TokenService::class);
|
|
$tokens->method('createToken')->willReturn('token');
|
|
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->once())
|
|
->method('dispatch')
|
|
->with(self::callback(static fn($event): bool =>
|
|
$event instanceof AuthenticationSucceededEvent
|
|
&& $event->getUserId() === 'user-a'
|
|
&& $event->getTenantId() === 'tenant-a'
|
|
));
|
|
|
|
$manager = new AuthenticationManager(
|
|
$tenant,
|
|
$cache,
|
|
$providers,
|
|
$tokens,
|
|
$users,
|
|
$events,
|
|
);
|
|
|
|
$response = $manager->handle(AuthenticationRequest::verify(
|
|
'auth-session',
|
|
'password',
|
|
'correct',
|
|
));
|
|
|
|
self::assertTrue($response->isSuccess());
|
|
}
|
|
|
|
#[Test]
|
|
public function rejectedCredentialPublishesAuthenticationFailedEvent(): void
|
|
{
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('configuration')->willReturn(new TenantConfiguration());
|
|
|
|
$session = new AuthenticationSession(
|
|
id: 'auth-session',
|
|
state: AuthenticationSession::STATE_IDENTIFIED,
|
|
tenantIdentifier: 'tenant-a',
|
|
userIdentifier: 'user-a',
|
|
userIdentity: 'person@example.com',
|
|
methodsAvailable: ['password'],
|
|
createdAt: time(),
|
|
expiresAt: time() + 300,
|
|
);
|
|
|
|
$cache = $this->createMock(EphemeralCacheInterface::class);
|
|
$cache->expects($this->once())
|
|
->method('get')
|
|
->with('auth-session', CacheScope::Tenant, 'auth')
|
|
->willReturn($session);
|
|
$cache->expects($this->once())
|
|
->method('set')
|
|
->willReturn(true);
|
|
|
|
$provider = new class implements AuthenticationProviderInterface {
|
|
public int $verificationCount = 0;
|
|
|
|
public function type(): string
|
|
{
|
|
return 'authentication';
|
|
}
|
|
|
|
public function identifier(): string
|
|
{
|
|
return 'password';
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return 'Password';
|
|
}
|
|
|
|
public function description(): string
|
|
{
|
|
return 'Test provider';
|
|
}
|
|
|
|
public function method(): string
|
|
{
|
|
return self::METHOD_CREDENTIAL;
|
|
}
|
|
|
|
public function icon(): string
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function verify(ProviderContext $context, string $secret): ProviderResult
|
|
{
|
|
$this->verificationCount++;
|
|
return ProviderResult::failed(ProviderResult::ERROR_INVALID_FACTOR);
|
|
}
|
|
|
|
public function beginChallenge(ProviderContext $context): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function verifyChallenge(ProviderContext $context, string $code): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function beginRedirect(
|
|
ProviderContext $context,
|
|
string $callbackUrl,
|
|
?string $returnUrl = null,
|
|
): ProviderResult {
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function completeRedirect(ProviderContext $context, array $params): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
};
|
|
|
|
$providers = $this->createMock(ProviderManager::class);
|
|
$providers->method('resolve')->with('authentication', 'password')->willReturn($provider);
|
|
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$events->expects($this->once())
|
|
->method('dispatch')
|
|
->with(self::callback(static function ($event): bool {
|
|
return $event instanceof AuthenticationFailedEvent
|
|
&& $event->getUserId() === 'user-a'
|
|
&& $event->getReason() === ProviderResult::ERROR_INVALID_FACTOR
|
|
&& $event->getTenantId() === 'tenant-a';
|
|
}));
|
|
|
|
$manager = new AuthenticationManager(
|
|
$tenant,
|
|
$cache,
|
|
$providers,
|
|
$this->createStub(TokenService::class),
|
|
$this->createStub(UserAccountsService::class),
|
|
$events,
|
|
);
|
|
|
|
$response = $manager->handle(AuthenticationRequest::verify(
|
|
'auth-session',
|
|
'password',
|
|
'incorrect',
|
|
));
|
|
|
|
self::assertTrue($response->isFailed());
|
|
self::assertSame(1, $provider->verificationCount);
|
|
}
|
|
}
|
|
|
|
final class SuccessfulAuthenticationProvider implements AuthenticationProviderInterface
|
|
{
|
|
public function type(): string
|
|
{
|
|
return 'authentication';
|
|
}
|
|
|
|
public function identifier(): string
|
|
{
|
|
return 'password';
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return 'Password';
|
|
}
|
|
|
|
public function description(): string
|
|
{
|
|
return 'Successful test provider';
|
|
}
|
|
|
|
public function method(): string
|
|
{
|
|
return self::METHOD_CREDENTIAL;
|
|
}
|
|
|
|
public function icon(): string
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function verify(ProviderContext $context, string $secret): ProviderResult
|
|
{
|
|
return ProviderResult::success();
|
|
}
|
|
|
|
public function beginChallenge(ProviderContext $context): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function verifyChallenge(ProviderContext $context, string $code): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function beginRedirect(
|
|
ProviderContext $context,
|
|
string $callbackUrl,
|
|
?string $returnUrl = null,
|
|
): ProviderResult {
|
|
return ProviderResult::failed();
|
|
}
|
|
|
|
public function completeRedirect(ProviderContext $context, array $params): ProviderResult
|
|
{
|
|
return ProviderResult::failed();
|
|
}
|
|
}
|