From 42a5897d297be881f608e53a886318581eaa150d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 22 Dec 2025 17:07:07 -0500 Subject: [PATCH] authentication provider provisioning --- lib/Provider.php | 180 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 166 insertions(+), 14 deletions(-) diff --git a/lib/Provider.php b/lib/Provider.php index 56b7476..50a387e 100644 --- a/lib/Provider.php +++ b/lib/Provider.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\AuthenticationProviderOidc; +use KTXC\Service\UserService; use KTXF\Security\Authentication\AuthenticationProviderAbstract; use KTXF\Security\Authentication\ProviderContext; use KTXF\Security\Authentication\ProviderResult; @@ -18,6 +19,7 @@ class Provider extends AuthenticationProviderAbstract { public function __construct( private readonly OidcClient $client, + private readonly UserService $userService, ) { } // ========================================================================= @@ -114,13 +116,21 @@ class Provider extends AuthenticationProviderAbstract ); } + // Provision/update user directly + $user = $this->provisionUser($result, $config); + + if (!$user) { + return ProviderResult::failed( + ProviderResult::ERROR_INTERNAL, + 'Failed to provision user from OIDC identity' + ); + } + + // Return success with user identifier return ProviderResult::success( [ - 'provider' => $this->identifier(), - 'subject' => $result['sub'] ?? null, - 'email' => $result['email'] ?? null, - 'name' => $result['name'] ?? null, - 'attributes' => $result['attributes'] ?? [], + 'user_identifier' => $user['uid'], + 'user_identity' => $user['identity'], ], [ 'return_url' => $context->getMeta('return_url'), @@ -135,23 +145,165 @@ class Provider extends AuthenticationProviderAbstract } // ========================================================================= - // Attribute Helpers + // Provisioning Helpers // ========================================================================= - public function getAttributes(array $identity): array + /** + * Provision or update user from OIDC identity + */ + private function provisionUser(array $oidcData, array $config): ?array { - return $identity['attributes'] ?? []; + $subject = $oidcData['sub'] ?? null; + $email = $oidcData['email'] ?? null; + + if (!$subject || !$email) { + return null; + } + + // Check if user exists by provider subject + $user = $this->userService->fetchByProviderSubject($this->identifier(), $subject); + + if ($user) { + // Update existing user + return $this->updateUser($user, $oidcData, $config); + } + + // Check if user exists by email + $user = $this->userService->fetchByIdentityRaw($email); + + if ($user) { + // Link existing user to this OIDC provider + return $this->linkUser($user, $oidcData, $config); + } + + // Create new user + return $this->createUser($oidcData, $config); } - public function mapAttributes(array $attributes, array $mapping): array + /** + * Create new user from OIDC data + */ + private function createUser(array $oidcData, array $config): array { - $mapped = []; - foreach ($mapping as $sourceKey => $targetKey) { - if (isset($attributes[$sourceKey])) { - $mapped[$targetKey] = $attributes[$sourceKey]; + $managedFields = $this->getManagedFields($config); + + $userData = [ + 'identity' => $oidcData['email'], + 'label' => $oidcData['name'] ?? $oidcData['email'], + 'enabled' => true, + 'provider' => $this->identifier(), + 'provider_subject' => $oidcData['sub'], + 'provider_synced_at' => time(), + 'provider_managed_fields' => $managedFields, + 'profile' => [], + ]; + + // Map OIDC attributes to profile + $mapping = [ + 'given_name' => 'name_given', + 'family_name' => 'name_family', + 'picture' => 'avatar', + 'email' => 'email', + 'phone_number' => 'phone', + ]; + + foreach ($mapping as $oidcField => $profileField) { + if (isset($oidcData[$oidcField])) { + $userData['profile'][$profileField] = $oidcData[$oidcField]; } } - return $mapped; + + return $this->userService->createUser($userData); + } + + /** + * Update existing user with OIDC data + */ + private function updateUser(array $user, array $oidcData, array $config): array + { + $managedFields = $this->getManagedFields($config); + + // Update provider sync metadata + $this->userService->updateUser($user['uid'], [ + 'provider_synced_at' => time(), + 'provider_managed_fields' => $managedFields, + ]); + + // Update label if provided + if (isset($oidcData['name'])) { + $this->userService->updateUser($user['uid'], [ + 'label' => $oidcData['name'], + ]); + } + + // Update profile fields (only managed ones will be updated due to storeProfile filtering) + $profileUpdates = []; + $mapping = [ + 'given_name' => 'name_given', + 'family_name' => 'name_family', + 'picture' => 'avatar', + ]; + + foreach ($mapping as $oidcField => $profileField) { + if (isset($oidcData[$oidcField]) && in_array($profileField, $managedFields)) { + $profileUpdates[$profileField] = $oidcData[$oidcField]; + } + } + + if (!empty($profileUpdates)) { + // Use updateUser with profile. notation since these are managed fields + $updates = []; + foreach ($profileUpdates as $field => $value) { + $updates["profile.{$field}"] = $value; + } + $this->userService->updateUser($user['uid'], $updates); + } + + return $this->userService->fetchByIdentifier($user['uid']); + } + + /** + * Link existing local user to OIDC provider + */ + private function linkUser(array $user, array $oidcData, array $config): array + { + $managedFields = $this->getManagedFields($config); + + // Link existing local user to OIDC provider + $this->userService->updateUser($user['uid'], [ + 'provider' => $this->identifier(), + 'provider_subject' => $oidcData['sub'], + 'provider_synced_at' => time(), + 'provider_managed_fields' => $managedFields, + ]); + + // Then update with OIDC data + return $this->updateUser( + $this->userService->fetchByIdentifier($user['uid']), + $oidcData, + $config + ); + } + + /** + * Get default managed fields for OIDC provider + */ + private function managedFields(): array + { + return [ + 'name_given', + 'name_family', + 'avatar', + ]; + } + + /** + * Get managed fields from config or use defaults + */ + private function getManagedFields(array $config): array + { + // Allow configuration to override default managed fields + return $config['managed_fields'] ?? $this->managedFields(); } }