feat(auth): add domain-based SSO discovery for unprovisioned users

Users who exist in Microsoft Entra ID or LDAP but have no local account
were blocked at the email-first login step because the system required a
local user record before showing SSO options. This adds a domain-based
fallback: when the email is unknown locally, the system checks tenant SSO
configurations for matching allowed_domains and presents SSO-only login
options, enabling the existing JIT provisioning to trigger on callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 14:35:57 +02:00
parent cacd1fb6d1
commit 1c784efd5c
7 changed files with 325 additions and 11 deletions

View File

@@ -154,6 +154,64 @@ class TenantSsoService
return true;
}
/**
* Discover tenants that accept SSO login for the given email domain.
* Only tenants with explicitly configured (non-empty) allowed_domains are returned.
* Returns SSO-only methods (local is always false since the user has no local account).
*
* @return list<array{id: int, methods: array}> Tenant candidates with SSO methods
*/
public function discoverTenantsByEmailDomain(string $email): array
{
$email = strtolower(trim($email));
$parts = explode('@', $email);
if (count($parts) !== 2 || $parts[1] === '') {
return [];
}
$domain = $parts[1];
$microsoftMatches = $this->tenantMicrosoftAuthRepository->findEnabledWithExplicitDomain($domain);
$ldapMatches = $this->tenantLdapAuthRepository->findEnabledWithExplicitDomain($domain);
$tenantIds = [];
foreach ($microsoftMatches as $match) {
$tenantIds[(int) $match['tenant_id']] = true;
}
foreach ($ldapMatches as $match) {
$tenantIds[(int) $match['tenant_id']] = true;
}
if (!$tenantIds) {
return [];
}
$candidates = [];
foreach (array_keys($tenantIds) as $tenantId) {
$tenant = $this->tenantGateway->findById($tenantId);
if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
continue;
}
$methods = $this->resolveTenantLoginMethods($tenantId);
// Discovery candidates never offer local password (user has no local account)
$methods['local'] = false;
// Skip tenants where no SSO method is actually functional
if (empty($methods['microsoft']) && empty($methods['ldap'])) {
continue;
}
$candidates[] = [
'id' => $tenantId,
'tenant' => $tenant,
'methods' => $methods,
];
}
return $candidates;
}
public function resolveTenantLoginMethods(int $tenantId): array
{
$tenant = $this->tenantGateway->findById($tenantId);