feat(auth): sync display name and job title from Microsoft Graph, fix tenant SSO form handling

Fetches displayName/jobTitle from Graph API and syncs them via SSO profile sync. Also fixes LDAP enabled state in tenant form, adds LDAP save on tenant create, and hardens LDAP port validation precedence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 14:13:21 +02:00
parent 312d43d9d9
commit 4e359fe659
9 changed files with 39 additions and 10 deletions

View File

@@ -225,13 +225,13 @@ class UserWriteRepository implements UserWriteRepositoryInterface
return false;
}
$row = DB::selectOne('select first_name, last_name, email, phone, mobile from users where id = ? limit 1', (string) $id);
$row = DB::selectOne('select first_name, last_name, email, phone, mobile, job_title, display_name from users where id = ? limit 1', (string) $id);
$existing = $row['users'] ?? null;
if (!is_array($existing)) {
return false;
}
$allowed = ['first_name', 'last_name', 'phone', 'mobile'];
$allowed = ['first_name', 'last_name', 'display_name', 'job_title', 'phone', 'mobile'];
$updates = [];
foreach ($allowed as $field) {
if (!array_key_exists($field, $fields)) {
@@ -251,7 +251,7 @@ class UserWriteRepository implements UserWriteRepositoryInterface
return true;
}
if (isset($updates['first_name']) || isset($updates['last_name'])) {
if (!isset($updates['display_name']) && (isset($updates['first_name']) || isset($updates['last_name']))) {
$displayData = [
'first_name' => $updates['first_name'] ?? (string) ($existing['first_name'] ?? ''),
'last_name' => $updates['last_name'] ?? (string) ($existing['last_name'] ?? ''),

View File

@@ -197,6 +197,8 @@ class MicrosoftOidcService
'preferred_language' => trim((string) ($claims['preferred_language'] ?? '')),
'graph_given_name' => trim((string) ($graphProfile['given_name'] ?? '')),
'graph_family_name' => trim((string) ($graphProfile['family_name'] ?? '')),
'graph_display_name' => trim((string) ($graphProfile['display_name'] ?? '')),
'graph_job_title' => trim((string) ($graphProfile['job_title'] ?? '')),
'graph_phone' => trim((string) ($graphProfile['phone'] ?? '')),
'graph_mobile' => trim((string) ($graphProfile['mobile'] ?? '')),
'graph_avatar_mime' => trim((string) ($graphAvatar['mime'] ?? '')),
@@ -368,7 +370,7 @@ class MicrosoftOidcService
private function fetchMicrosoftGraphProfile(string $accessToken): array
{
$url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,mobilePhone,businessPhones';
$url = 'https://graph.microsoft.com/v1.0/me?$select=givenName,surname,displayName,jobTitle,mobilePhone,businessPhones';
$response = $this->oidcHttpGateway->call('GET', $url, '', [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $accessToken,
@@ -393,6 +395,8 @@ class MicrosoftOidcService
'profile' => [
'given_name' => trim((string) ($data['givenName'] ?? '')),
'family_name' => trim((string) ($data['surname'] ?? '')),
'display_name' => trim((string) ($data['displayName'] ?? '')),
'job_title' => trim((string) ($data['jobTitle'] ?? '')),
'mobile' => trim((string) ($data['mobilePhone'] ?? '')),
'phone' => $phone,
],

View File

@@ -153,6 +153,8 @@ class SsoUserLinkService
'last_name' => trim((string) ($claims['family_name'] ?? '')) !== ''
? trim((string) ($claims['family_name'] ?? ''))
: trim((string) ($claims['graph_family_name'] ?? '')),
'display_name' => trim((string) ($claims['graph_display_name'] ?? '')),
'job_title' => trim((string) ($claims['graph_job_title'] ?? '')),
'phone' => trim((string) ($claims['graph_phone'] ?? '')),
'mobile' => trim((string) ($claims['graph_mobile'] ?? '')),
];

View File

@@ -13,6 +13,8 @@ class TenantSsoService
private const PROFILE_SYNC_FIELD_OPTIONS = [
'first_name' => 'Sync first name',
'last_name' => 'Sync last name',
'display_name' => 'Sync display name',
'job_title' => 'Sync job title',
'phone' => 'Sync phone',
'mobile' => 'Sync mobile',
'avatar' => 'Sync avatar image',
@@ -459,7 +461,9 @@ class TenantSsoService
$fields = $this->normalizeProfileSyncFields($fields);
return in_array('phone', $fields, true)
|| in_array('mobile', $fields, true)
|| in_array('avatar', $fields, true);
|| in_array('avatar', $fields, true)
|| in_array('job_title', $fields, true)
|| in_array('display_name', $fields, true);
}
public function shouldAutoRememberOnMicrosoftLogin(int $tenantId): bool
@@ -784,7 +788,7 @@ class TenantSsoService
if ($enabled && $baseDn === '') {
$errors[] = t('LDAP base DN is required');
}
if ($enabled && $port < 1 || $port > 65535) {
if ($enabled && ($port < 1 || $port > 65535)) {
$errors[] = t('LDAP port must be between 1 and 65535');
}
if ($enabled && !str_contains($userSearchFilter, '%s')) {
@@ -832,7 +836,11 @@ class TenantSsoService
}
$autoRememberMode = $this->normalizeAutoRememberMode($input['ldap_auto_remember_mode'] ?? null);
$attributeMapJson = json_encode($attributeMap, JSON_THROW_ON_ERROR);
try {
$attributeMapJson = json_encode($attributeMap, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return ['ok' => false, 'errors' => [t('LDAP attribute map could not be encoded')]];
}
$saved = $this->tenantLdapAuthRepository->upsertByTenantId($tenantId, [
'enabled' => $enabled,