tenant->identifier(); if (!$tenantId) { return null; } // Map attributes to user fields $mappedData = $this->mapAttributes($attributes, $providerConfig['attribute_map'] ?? []); // Validate required fields $identity = $mappedData['identity'] ?? $attributes['identity'] ?? $attributes['email'] ?? null; if (!$identity) { return null; } // Generate user ID $userId = UUID::v4(); // Build user data $userData = [ 'tid' => $tenantId, 'uid' => $userId, 'identity' => $identity, 'label' => $mappedData['label'] ?? $attributes['label'] ?? $attributes['name'] ?? $identity, 'enabled' => true, 'provider' => $providerId, 'external_subject' => $attributes['external_subject'] ?? null, 'roles' => $providerConfig['default_roles'] ?? [], 'profile' => $mappedData['profile'] ?? [], 'settings' => [], 'initial_login' => time(), 'recent_login' => time(), ]; // Create the user $createdUserId = $this->userStore->create($userData); if (!$createdUserId) { return null; } // Link external identity if we have an external subject if (!empty($attributes['external_subject'])) { $this->externalIdentityStore->linkIdentity( $tenantId, $userId, $providerId, $attributes['external_subject'], $attributes['raw'] ?? $attributes ); } // Build and return User object $user = new User(); $user->populate($userData, 'users'); return $user; } /** * Synchronize user profile with attributes from provider * Called on each login to keep profile data up to date * * @param User $user The existing user * @param array $attributes Attributes from provider * @param array $attributeMap Attribute mapping configuration * @return bool Whether sync was successful */ public function syncProfile(User $user, array $attributes, array $attributeMap = []): bool { $tenantId = $this->tenant->identifier(); $userId = $user->getId(); if (!$tenantId || !$userId) { return false; } // Map attributes $mappedData = $this->mapAttributes($attributes, $attributeMap); // Update profile fields if we have mapped profile data if (!empty($mappedData['profile'])) { $this->userStore->updateProfile($tenantId, $userId, $mappedData['profile']); } // Update label if provided and different if (!empty($mappedData['label']) && $mappedData['label'] !== $user->getLabel()) { $this->userStore->updateLabel($tenantId, $userId, $mappedData['label']); } // Always update last login $this->userStore->updateLastLogin($tenantId, $userId); // Update external identity attributes if applicable if (!empty($attributes['external_subject'])) { $this->externalIdentityStore->updateLastLogin( $tenantId, $user->getProvider() ?? '', $attributes['external_subject'] ); $this->externalIdentityStore->updateAttributes( $tenantId, $user->getProvider() ?? '', $attributes['external_subject'], $attributes['raw'] ?? $attributes ); } return true; } /** * Link an external identity to an existing user * * @param User $user The user to link * @param string $providerId Provider identifier * @param string $externalSubject External subject identifier * @param array $attributes Optional attributes from provider * @return bool Whether linking was successful */ public function linkExternalIdentity(User $user, string $providerId, string $externalSubject, array $attributes = []): bool { $tenantId = $this->tenant->identifier(); $userId = $user->getId(); if (!$tenantId || !$userId) { return false; } return $this->externalIdentityStore->linkIdentity( $tenantId, $userId, $providerId, $externalSubject, $attributes ); } /** * Find user by external identity * * @param string $providerId Provider identifier * @param string $externalSubject External subject identifier * @return User|null The user or null if not found */ public function findByExternalIdentity(string $providerId, string $externalSubject): ?User { $tenantId = $this->tenant->identifier(); if (!$tenantId) { return null; } // Look up in external identities table $externalIdentity = $this->externalIdentityStore->findByExternalSubject( $tenantId, $providerId, $externalSubject ); if (!$externalIdentity) { return null; } // Fetch the linked user $userData = $this->userStore->fetchByIdentifier($tenantId, $externalIdentity['uid']); if (!$userData) { return null; } $user = new User(); $user->populate($userData, 'users'); return $user; } /** * Map provider attributes to user fields using attribute map * * @param array $attributes Raw attributes from provider * @param array $attributeMap Mapping configuration {source_attr: target_field} * @return array Mapped data with 'identity', 'label', 'profile' keys */ protected function mapAttributes(array $attributes, array $attributeMap): array { $result = [ 'identity' => null, 'label' => null, 'profile' => [], ]; foreach ($attributeMap as $sourceAttr => $targetField) { // Get source value (supports nested attributes with dot notation) $value = $this->getNestedValue($attributes, $sourceAttr); if ($value === null) { continue; } // Set target value (supports nested targets with dot notation) if ($targetField === 'identity') { $result['identity'] = $value; } elseif ($targetField === 'label') { $result['label'] = $value; } elseif (str_starts_with($targetField, 'profile.')) { $profileField = substr($targetField, 8); $result['profile'][$profileField] = $value; } } return $result; } /** * Get nested value from array using dot notation * * @param array $array Source array * @param string $key Key with optional dot notation (e.g., 'user.email') * @return mixed|null Value or null if not found */ protected function getNestedValue(array $array, string $key): mixed { $keys = explode('.', $key); $value = $array; foreach ($keys as $k) { if (!is_array($value) || !array_key_exists($k, $value)) { return null; } $value = $value[$k]; } return $value; } /** * Check if auto-provisioning is enabled for a provider * * @param string $providerId Provider identifier * @return bool */ public function isAutoProvisioningEnabled(string $providerId): bool { $config = $this->tenant->identityProviderConfig($providerId); return ($config['provisioning'] ?? 'manual') === 'auto'; } }