config; if (empty($config)) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_PROVIDER, 'OIDC provider configuration is missing' ); } try { $authData = $this->client->beginAuth($config, $callbackUrl, $returnUrl); return ProviderResult::redirect( $authData['redirect_url'], [ 'state' => $authData['state'], 'nonce' => $authData['nonce'] ?? null, 'return_url' => $returnUrl, ] ); } catch (\Throwable $e) { return ProviderResult::failed( ProviderResult::ERROR_INTERNAL, 'Failed to initiate OIDC authentication: ' . $e->getMessage() ); } } public function completeRedirect(ProviderContext $context, array $params): ProviderResult { $config = $context->config; $expectedState = $context->getMeta('state'); $expectedNonce = $context->getMeta('nonce'); if (empty($config)) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_PROVIDER, 'OIDC provider configuration is missing' ); } if (empty($expectedState)) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_CREDENTIALS, 'Missing expected state for OIDC verification' ); } try { $result = $this->client->completeAuth($params, $config, $expectedState, $expectedNonce); if (!$result || !isset($result['success']) || !$result['success']) { return ProviderResult::failed( ProviderResult::ERROR_INVALID_CREDENTIALS, $result['error'] ?? 'OIDC authentication failed' ); } // 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( [ 'user_identifier' => $user['uid'], 'user_identity' => $user['identity'], ], [ 'return_url' => $context->getMeta('return_url'), ] ); } catch (\Throwable $e) { return ProviderResult::failed( ProviderResult::ERROR_INTERNAL, 'Failed to complete OIDC authentication: ' . $e->getMessage() ); } } // ========================================================================= // Provisioning Helpers // ========================================================================= /** * Provision or update user from OIDC identity */ private function provisionUser(array $oidcData, array $config): ?array { $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); } /** * Create new user from OIDC data */ private function createUser(array $oidcData, array $config): array { $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 $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(); } }