807 lines
27 KiB
PHP
807 lines
27 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXC\Security;
|
|
|
|
use KTXC\Models\Identity\User;
|
|
use KTXC\Resource\ProviderManager;
|
|
use KTXC\Security\Authentication\AuthenticationRequest;
|
|
use KTXC\Security\Authentication\AuthenticationResponse;
|
|
use KTXC\Service\TokenService;
|
|
use KTXC\Service\UserService;
|
|
use KTXC\SessionTenant;
|
|
use KTXF\Cache\CacheScope;
|
|
use KTXF\Cache\EphemeralCacheInterface;
|
|
use KTXF\Security\Authentication\AuthenticationProviderInterface;
|
|
use KTXF\Security\Authentication\AuthenticationSession;
|
|
use KTXF\Security\Authentication\ProviderContext;
|
|
|
|
/**
|
|
* Authentication Manager
|
|
*/
|
|
class AuthenticationManager
|
|
{
|
|
private const CACHE_USAGE = 'auth';
|
|
private string $securityCode;
|
|
|
|
public function __construct(
|
|
private readonly SessionTenant $tenant,
|
|
private readonly EphemeralCacheInterface $cache,
|
|
private readonly ProviderManager $providerManager,
|
|
private readonly TokenService $tokenService,
|
|
private readonly UserService $userService,
|
|
) {
|
|
$this->securityCode = $this->tenant->configuration()->security()->code();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Main Entry Point
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Handle an authentication request
|
|
*/
|
|
public function handle(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
return match ($request->action) {
|
|
AuthenticationRequest::ACTION_START => $this->handleStart(),
|
|
AuthenticationRequest::ACTION_IDENTIFY => $this->handleIdentify($request),
|
|
AuthenticationRequest::ACTION_VERIFY => $this->handleVerify($request),
|
|
AuthenticationRequest::ACTION_CHALLENGE => $this->handleChallenge($request),
|
|
AuthenticationRequest::ACTION_REDIRECT => $this->handleRedirect($request),
|
|
AuthenticationRequest::ACTION_CALLBACK => $this->handleCallback($request),
|
|
AuthenticationRequest::ACTION_STATUS => $this->handleStatus($request),
|
|
AuthenticationRequest::ACTION_CANCEL => $this->handleCancel($request),
|
|
AuthenticationRequest::ACTION_REFRESH => $this->handleRefresh($request),
|
|
AuthenticationRequest::ACTION_LOGOUT => $this->handleLogout($request),
|
|
default => AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_REQUEST,
|
|
'Unknown action',
|
|
400
|
|
),
|
|
};
|
|
}
|
|
|
|
// =========================================================================
|
|
// Action Handlers
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Start a new authentication session
|
|
*/
|
|
private function handleStart(): AuthenticationResponse
|
|
{
|
|
$methods = $this->methodsConfigured();
|
|
|
|
$session = AuthenticationSession::create(
|
|
$this->tenant->identifier(),
|
|
AuthenticationSession::STATE_FRESH
|
|
);
|
|
|
|
$this->saveSession($session);
|
|
|
|
return AuthenticationResponse::started($session->id, $methods);
|
|
}
|
|
|
|
/**
|
|
* Identify user (identity-first flow)
|
|
*/
|
|
private function handleIdentify(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Invalid or expired session',
|
|
401
|
|
);
|
|
}
|
|
|
|
// Return all tenant methods to prevent enumeration
|
|
// Filter to non-redirect methods since redirects don't need identity first
|
|
$methods = $this->methodsConfigured();
|
|
$methods = array_values(array_filter($methods, fn($m) => $m['method'] !== 'redirect'));
|
|
$require = $this->tenant->configuration()->authentication()->methodsMinimal();
|
|
|
|
// Store identity in session without validating to prevent enumeration
|
|
$session->setMethods(array_column($methods, 'id'), $require);
|
|
$session->setIdentity($request->identity);
|
|
$this->saveSession($session);
|
|
|
|
return AuthenticationResponse::identified($session->id, $session->state(), $methods);
|
|
}
|
|
|
|
/**
|
|
* Verify credentials or challenge response
|
|
*/
|
|
private function handleVerify(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Invalid or expired session',
|
|
401
|
|
);
|
|
}
|
|
|
|
if (empty($session->userIdentity)) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_SESSION,
|
|
'Identity is required',
|
|
400
|
|
);
|
|
}
|
|
|
|
$method = $request->method;
|
|
|
|
if (!$session->methodEligible($method)) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_REQUEST,
|
|
'Method not available',
|
|
400
|
|
);
|
|
}
|
|
|
|
$provider = $this->providerManager->resolve('authentication', $method);
|
|
if (!$provider instanceof AuthenticationProviderInterface) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider not available',
|
|
400
|
|
);
|
|
}
|
|
|
|
// Build provider context
|
|
$context = $this->buildProviderContext($session, $method);
|
|
|
|
// Call appropriate provider method based on provider type
|
|
$providerMethod = $provider->method();
|
|
|
|
if ($providerMethod === AuthenticationProviderInterface::METHOD_CREDENTIAL) {
|
|
$result = $provider->verify($context, $request->secret);
|
|
} elseif ($providerMethod === AuthenticationProviderInterface::METHOD_CHALLENGE) {
|
|
$result = $provider->verifyChallenge($context, $request->secret);
|
|
} else {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider cannot be used for direct verification',
|
|
400
|
|
);
|
|
}
|
|
|
|
// Store any session data from provider
|
|
if (!empty($result->sessionData)) {
|
|
$session->setMeta("provider:{$method}", $result->sessionData);
|
|
}
|
|
|
|
if (!$result->isSuccess()) {
|
|
$this->saveSession($session);
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
|
'Authentication failed. If you haven\'t set up this method, try another option.',
|
|
401
|
|
);
|
|
}
|
|
|
|
// Resolve user if not yet set
|
|
if ($session->userIdentifier === null) {
|
|
$user = $this->userService->fetchByIdentity($session->userIdentity);
|
|
if ($user === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
|
'User not found',
|
|
401
|
|
);
|
|
}
|
|
$session->userIdentifier = $user->getId();
|
|
}
|
|
|
|
// Mark method complete
|
|
$session->methodCompleted($method);
|
|
$this->saveSession($session);
|
|
|
|
// Check if all required factors are complete
|
|
if ($session->state() !== AuthenticationSession::STATE_COMPLETE) {
|
|
$remainingMethods = $this->methodsConfigured($session->methodsCompleted);
|
|
// Filter out redirect methods - they can't be used as secondary factors
|
|
$remainingMethods = array_values(array_filter(
|
|
$remainingMethods,
|
|
fn($m) => $m['method'] !== 'redirect'
|
|
));
|
|
return AuthenticationResponse::pending($session->id, $remainingMethods);
|
|
}
|
|
|
|
// Authentication complete - issue tokens
|
|
return $this->completeAuthentication($session);
|
|
}
|
|
|
|
/**
|
|
* Begin a challenge (SMS, email, TOTP preparation)
|
|
*/
|
|
private function handleChallenge(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Invalid or expired session',
|
|
401
|
|
);
|
|
}
|
|
|
|
$method = $request->method;
|
|
|
|
// Resolve user identifier if needed
|
|
if ($session->userIdentifier === null && $session->userIdentity) {
|
|
$user = $this->userService->fetchByIdentity($session->userIdentity);
|
|
if ($user) {
|
|
$session->userIdentifier = $user->getId();
|
|
$this->saveSession($session);
|
|
}
|
|
}
|
|
|
|
$provider = $this->providerManager->resolve('authentication', $method);
|
|
if (!$provider instanceof AuthenticationProviderInterface) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider not available',
|
|
400
|
|
);
|
|
}
|
|
|
|
$context = $this->buildProviderContext($session, $method);
|
|
$result = $provider->beginChallenge($context);
|
|
|
|
// Store any session data from provider
|
|
if (!empty($result->sessionData)) {
|
|
$session->setMeta("provider:{$method}", $result->sessionData);
|
|
$this->saveSession($session);
|
|
}
|
|
|
|
if ($result->isChallenge()) {
|
|
return AuthenticationResponse::challenge(
|
|
$session->id,
|
|
$result->getClientData('challenge', [])
|
|
);
|
|
}
|
|
|
|
if ($result->isFailed()) {
|
|
// Generic error to prevent enumeration
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
|
'Authentication failed. If you haven\'t set up this method, try another option.',
|
|
401
|
|
);
|
|
}
|
|
|
|
// Unexpected result
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INTERNAL,
|
|
'Unexpected provider response',
|
|
500
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Begin redirect-based authentication (OIDC/SAML)
|
|
*/
|
|
private function handleRedirect(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Invalid or expired session',
|
|
401
|
|
);
|
|
}
|
|
|
|
$method = $request->method;
|
|
|
|
$provider = $this->providerManager->resolve('authentication', $method);
|
|
if (!$provider instanceof AuthenticationProviderInterface) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider not available',
|
|
400
|
|
);
|
|
}
|
|
|
|
if ($provider->method() !== AuthenticationProviderInterface::METHOD_REDIRECT) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider does not support redirect authentication',
|
|
400
|
|
);
|
|
}
|
|
|
|
$context = $this->buildProviderContext($session, $method);
|
|
$result = $provider->beginRedirect($context, $request->callbackUrl, $request->returnUrl);
|
|
|
|
if ($result->isFailed()) {
|
|
return AuthenticationResponse::failed(
|
|
$result->errorCode ?? AuthenticationResponse::ERROR_INTERNAL,
|
|
$result->errorMessage ?? 'Failed to initiate redirect authentication',
|
|
500
|
|
);
|
|
}
|
|
|
|
// Store provider session data (state, nonce, etc.)
|
|
$session->setMeta("provider:{$method}", $result->sessionData);
|
|
$session->setMeta('redirect_method', $method);
|
|
$this->saveSession($session);
|
|
|
|
return AuthenticationResponse::redirect(
|
|
$session->id,
|
|
$result->getClientData('redirect_url')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Complete redirect-based authentication (callback from IdP)
|
|
*/
|
|
private function handleCallback(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Invalid or expired session',
|
|
401
|
|
);
|
|
}
|
|
|
|
$method = $request->method;
|
|
$expectedMethod = $session->getMeta('redirect_method');
|
|
|
|
if ($expectedMethod !== $method) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_SESSION,
|
|
'Provider mismatch',
|
|
400
|
|
);
|
|
}
|
|
|
|
$provider = $this->providerManager->resolve('authentication', $method);
|
|
if (!$provider instanceof AuthenticationProviderInterface) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_PROVIDER,
|
|
'Provider not available',
|
|
400
|
|
);
|
|
}
|
|
|
|
$context = $this->buildProviderContext($session, $method);
|
|
$result = $provider->completeRedirect($context, $request->params);
|
|
|
|
if ($result->isFailed()) {
|
|
$this->deleteSession($session->id);
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
|
$result->errorMessage ?? 'Authentication failed',
|
|
401
|
|
);
|
|
}
|
|
|
|
// Provider has already provisioned the user - just get user identifier
|
|
$userIdentifier = $result->identity['user_identifier'] ?? null;
|
|
|
|
if (!$userIdentifier) {
|
|
$this->deleteSession($session->id);
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INTERNAL,
|
|
'User provisioning failed',
|
|
500
|
|
);
|
|
}
|
|
|
|
// Load user
|
|
$userData = $this->userService->fetchByIdentifier($userIdentifier);
|
|
if (!$userData) {
|
|
$this->deleteSession($session->id);
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
|
'User not found after provisioning',
|
|
401
|
|
);
|
|
}
|
|
|
|
$user = new User();
|
|
$user->populate($userData, 'users');
|
|
|
|
// Set user in session
|
|
$session->userIdentifier = $user->getId();
|
|
$session->userIdentity = $user->getIdentity();
|
|
$session->methodCompleted($method);
|
|
|
|
// Check if MFA is required
|
|
$require = $this->tenant->configuration()->authentication()->methodsMinimal();
|
|
if ($require > 1) {
|
|
$remainingMethods = $this->methodsConfigured([$method]);
|
|
// Filter out redirect methods - they can't be used as secondary factors
|
|
$remainingMethods = array_values(array_filter(
|
|
$remainingMethods,
|
|
fn($m) => $m['method'] !== 'redirect'
|
|
));
|
|
$session->setMethods(array_column($remainingMethods, 'id'), $require);
|
|
$this->saveSession($session);
|
|
|
|
return AuthenticationResponse::pending($session->id, $remainingMethods);
|
|
}
|
|
|
|
// Authentication complete
|
|
return $this->completeAuthentication($session);
|
|
}
|
|
|
|
/**
|
|
* Get session status
|
|
*/
|
|
private function handleStatus(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$session = $this->retrieveSession($request->sessionId);
|
|
|
|
if ($session === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_SESSION_EXPIRED,
|
|
'Session not found or expired',
|
|
404
|
|
);
|
|
}
|
|
|
|
$methods = $this->methodsConfigured($session->methodsCompleted);
|
|
|
|
return AuthenticationResponse::status(
|
|
$session->id,
|
|
$session->state(),
|
|
$methods,
|
|
$session->userIdentity
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cancel session
|
|
*/
|
|
private function handleCancel(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
if ($request->sessionId) {
|
|
$this->deleteSession($request->sessionId);
|
|
}
|
|
|
|
return AuthenticationResponse::cancelled();
|
|
}
|
|
|
|
/**
|
|
* Refresh access token
|
|
*/
|
|
private function handleRefresh(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$payload = $this->tokenService->validateToken($request->token, $this->securityCode);
|
|
|
|
if (!$payload) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
|
'Invalid or expired refresh token',
|
|
401
|
|
);
|
|
}
|
|
|
|
if (($payload['type'] ?? null) !== 'refresh') {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_INVALID_CREDENTIALS,
|
|
'Invalid token type',
|
|
401
|
|
);
|
|
}
|
|
|
|
$identifier = $payload['identifier'] ?? null;
|
|
$userData = $this->userService->fetchByIdentifier($identifier);
|
|
|
|
if ($userData === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
|
'User not found',
|
|
401
|
|
);
|
|
}
|
|
|
|
$user = new User();
|
|
$user->populate($userData, 'users');
|
|
|
|
$accessToken = $this->tokenService->createToken(
|
|
[
|
|
'tenant' => $this->tenant->identifier(),
|
|
'identifier' => $user->getId(),
|
|
'identity' => $user->getIdentity(),
|
|
'label' => $user->getLabel(),
|
|
'permissions' => $user->getPermissions(),
|
|
'mfa_verified' => true,
|
|
],
|
|
$this->securityCode,
|
|
900
|
|
);
|
|
|
|
return AuthenticationResponse::success(
|
|
$this->buildUserData($user),
|
|
['access' => $accessToken]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Logout
|
|
*/
|
|
private function handleLogout(AuthenticationRequest $request): AuthenticationResponse
|
|
{
|
|
$allDevices = $request->params['all_devices'] ?? false;
|
|
|
|
if ($request->token) {
|
|
$payload = $this->tokenService->validateToken($request->token, $this->securityCode);
|
|
|
|
if ($payload) {
|
|
if ($allDevices && isset($payload['identity'])) {
|
|
$this->tokenService->blacklistUserTokensBefore($payload['identity'], time());
|
|
} elseif (isset($payload['jti'], $payload['exp'])) {
|
|
$this->tokenService->blacklist($payload['jti'], $payload['exp']);
|
|
}
|
|
}
|
|
}
|
|
|
|
return AuthenticationResponse::cancelled();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Helper Methods
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Build provider context from session
|
|
*/
|
|
private function buildProviderContext(AuthenticationSession $session, string $method): ProviderContext
|
|
{
|
|
return new ProviderContext(
|
|
tenantId: $session->tenantIdentifier,
|
|
userIdentifier: $session->userIdentifier,
|
|
userIdentity: $session->userIdentity,
|
|
metadata: $session->getMeta("provider:{$method}") ?? [],
|
|
config: $this->getProviderConfig($method),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get provider configuration
|
|
*/
|
|
private function getProviderConfig(string $method): array
|
|
{
|
|
$providers = $this->tenant->configuration()->authentication()->providers();
|
|
return $providers[$method]['config'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Complete authentication and issue tokens
|
|
*/
|
|
private function completeAuthentication(AuthenticationSession $session): AuthenticationResponse
|
|
{
|
|
$userData = $this->userService->fetchByIdentifier($session->userIdentifier);
|
|
|
|
if ($userData === null) {
|
|
return AuthenticationResponse::failed(
|
|
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
|
'User not found',
|
|
401
|
|
);
|
|
}
|
|
|
|
$user = new User();
|
|
$user->populate($userData, 'users');
|
|
|
|
$tokens = $this->createTokens($user, count($session->methodsCompleted) > 1);
|
|
|
|
$this->deleteSession($session->id);
|
|
|
|
return AuthenticationResponse::success(
|
|
$this->buildUserData($user),
|
|
$tokens
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build user data for response
|
|
*/
|
|
private function buildUserData(User $user): array
|
|
{
|
|
return [
|
|
'identifier' => $user->getId(),
|
|
'identity' => $user->getIdentity(),
|
|
'label' => $user->getLabel(),
|
|
'permissions' => $user->getPermissions(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get configured authentication methods
|
|
*/
|
|
private function methodsConfigured(array $methodsCompleted = []): array
|
|
{
|
|
$tenantProviders = $this->tenant->configuration()->authentication()->providers();
|
|
$methods = [];
|
|
|
|
foreach ($tenantProviders as $providerId => $providerConfiguration) {
|
|
if (!($providerConfiguration['enabled'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
if (in_array($providerId, $methodsCompleted, true)) {
|
|
continue;
|
|
}
|
|
|
|
$provider = $this->providerManager->resolve('authentication', $providerId);
|
|
if (!$provider instanceof AuthenticationProviderInterface) {
|
|
continue;
|
|
}
|
|
|
|
$methods[] = [
|
|
'id' => $providerId,
|
|
'method' => $provider->method(),
|
|
'label' => $providerConfiguration['label'] ?? $provider->label(),
|
|
'icon' => $providerConfiguration['icon'] ?? $provider->icon() ?? null,
|
|
];
|
|
}
|
|
|
|
return $methods;
|
|
}
|
|
|
|
/**
|
|
* Create JWT tokens
|
|
*/
|
|
private function createTokens(User $user, bool $mfaVerified = false): array
|
|
{
|
|
$payload = [
|
|
'tenant' => $this->tenant->identifier(),
|
|
'identifier' => $user->getId(),
|
|
'identity' => $user->getIdentity(),
|
|
'label' => $user->getLabel(),
|
|
'permissions' => $user->getPermissions(),
|
|
'mfa_verified' => $mfaVerified,
|
|
];
|
|
|
|
return [
|
|
'access' => $this->tokenService->createToken($payload, $this->securityCode, 900),
|
|
'refresh' => $this->tokenService->createToken(
|
|
[
|
|
'tenant' => $payload['tenant'],
|
|
'identifier' => $payload['identifier'],
|
|
'identity' => $payload['identity'],
|
|
'type' => 'refresh',
|
|
],
|
|
$this->securityCode,
|
|
604800
|
|
),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Find or provision user from external identity
|
|
*/
|
|
private function findOrProvisionUser(
|
|
string $providerId,
|
|
array $identity,
|
|
array $providerConfig
|
|
): ?User {
|
|
$userIdentity = $identity['email'] ?? $identity['identity'] ?? null;
|
|
$externalSubject = $identity['subject'] ?? $identity['sub'] ?? null;
|
|
$attributes = $identity['attributes'] ?? [];
|
|
$attributes['identity'] = $userIdentity;
|
|
$attributes['external_subject'] = $externalSubject;
|
|
|
|
/*
|
|
// Try to find by external subject first
|
|
if ($externalSubject) {
|
|
$user = $this->provisioningService->findByExternalIdentity($providerId, $externalSubject);
|
|
if ($user) {
|
|
$this->provisioningService->syncProfile(
|
|
$user,
|
|
$attributes,
|
|
$providerConfig['attribute_map'] ?? []
|
|
);
|
|
return $user;
|
|
}
|
|
}
|
|
|
|
// Try to find by identity
|
|
if ($userIdentity) {
|
|
$existingUser = $this->userService->fetchByIdentity($userIdentity);
|
|
if ($existingUser) {
|
|
if ($existingUser->getProvider() === $providerId) {
|
|
if ($externalSubject) {
|
|
$this->provisioningService->linkExternalIdentity(
|
|
$existingUser,
|
|
$providerId,
|
|
$externalSubject,
|
|
$attributes
|
|
);
|
|
}
|
|
$this->provisioningService->syncProfile(
|
|
$existingUser,
|
|
$attributes,
|
|
$providerConfig['attribute_map'] ?? []
|
|
);
|
|
return $existingUser;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Auto-provision if enabled
|
|
if ($this->provisioningService->isAutoProvisioningEnabled($providerId)) {
|
|
return $this->provisioningService->provisionUser(
|
|
$providerId,
|
|
$attributes,
|
|
$providerConfig
|
|
);
|
|
}
|
|
*/
|
|
|
|
return null;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Session Cache Helpers
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Retrieve authentication session from cache
|
|
*/
|
|
private function retrieveSession(?string $sessionId): ?AuthenticationSession
|
|
{
|
|
if (empty($sessionId)) {
|
|
return null;
|
|
}
|
|
|
|
$data = $this->cache->get($sessionId, CacheScope::Tenant, self::CACHE_USAGE);
|
|
|
|
if ($data === null) {
|
|
return null;
|
|
}
|
|
|
|
if ($data instanceof AuthenticationSession) {
|
|
if ($data->isExpired()) {
|
|
$this->deleteSession($sessionId);
|
|
return null;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Save authentication session to cache
|
|
*/
|
|
private function saveSession(AuthenticationSession $session): bool
|
|
{
|
|
$ttl = $session->expiresAt > 0 ? $session->expiresAt - time() : AuthenticationSession::DEFAULT_TTL;
|
|
|
|
return $this->cache->set(
|
|
$session->id,
|
|
$session,
|
|
CacheScope::Tenant,
|
|
self::CACHE_USAGE,
|
|
max($ttl, 60)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete authentication session from cache
|
|
*/
|
|
private function deleteSession(string $sessionId): bool
|
|
{
|
|
return $this->cache->delete($sessionId, CacheScope::Tenant, self::CACHE_USAGE);
|
|
}
|
|
}
|