feat: add LDAP authentication as per-tenant login method
Add LDAP as a third authentication option alongside local password and Microsoft Entra ID SSO. Per-tenant configuration supports Active Directory, OpenLDAP, and FreeIPA with configurable attribute mapping, search-then-bind and direct-bind methods, encrypted bind credentials (AES-256-GCM), profile sync on login, and user auto-provisioning via external identity linking. Includes admin UI for connection settings with test-connection button, login page LDAP form, 25 new PHPUnit tests, and de/en translations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ $tenantCandidates = [];
|
||||
$tenantCandidateById = [];
|
||||
$selectedTenantId = 0;
|
||||
$selectedTenant = null;
|
||||
$selectedTenantMethods = ['local' => false, 'microsoft' => false, 'microsoft_reason' => ''];
|
||||
$selectedTenantMethods = ['local' => false, 'microsoft' => false, 'microsoft_reason' => '', 'ldap' => false, 'ldap_reason' => ''];
|
||||
$errorBag = formErrors();
|
||||
$setLoginWarning = static function (string $message) use ($errorBag): void {
|
||||
$errorBag->addGlobal($message);
|
||||
@@ -209,7 +209,7 @@ if (requestInput()->method() === 'POST') {
|
||||
$tenantCandidateById = $resolved['candidate_by_id'];
|
||||
$postedTenantId = (int) (requestInput()->bodyAll()['tenant_id'] ?? 0);
|
||||
|
||||
if (in_array($postedStage, ['choose_tenant', 'login_password'], true)
|
||||
if (in_array($postedStage, ['choose_tenant', 'login_password', 'login_ldap'], true)
|
||||
&& ($postedTenantId <= 0 || !isset($tenantCandidateById[$postedTenantId]))) {
|
||||
$stage = 'choose_tenant';
|
||||
$setLoginWarning(t('We could not continue with these login details'));
|
||||
@@ -237,6 +237,7 @@ if (requestInput()->method() === 'POST') {
|
||||
$selectedTenantSlug = trim((string) $selectedTenant['slug']);
|
||||
$isMicrosoftOnlyTenant = empty($selectedTenantMethods['local'])
|
||||
&& !empty($selectedTenantMethods['microsoft'])
|
||||
&& empty($selectedTenantMethods['ldap'])
|
||||
&& $selectedTenantSlug !== '';
|
||||
|
||||
if (
|
||||
@@ -260,6 +261,96 @@ if (requestInput()->method() === 'POST') {
|
||||
$stage = count($tenantCandidates) === 1 ? 'methods' : 'choose_tenant';
|
||||
} elseif ($postedStage === 'choose_tenant') {
|
||||
$stage = 'methods';
|
||||
} elseif ($postedStage === 'login_ldap') {
|
||||
$stage = 'methods';
|
||||
$passwordAttemptKey = strtolower(trim($email)) . '|' . $clientIp;
|
||||
$passwordLimitResult = $rateLimiterService->isBlocked($loginPasswordScope, $passwordAttemptKey);
|
||||
if (!($passwordLimitResult['allowed'] ?? true)) {
|
||||
$setRateLimitWarning((int) ($passwordLimitResult['retry_after'] ?? 60));
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($selectedTenantMethods['ldap'])) {
|
||||
$setLoginWarning(t('LDAP login is not available for this tenant.'));
|
||||
} else {
|
||||
$ldapUsername = trim((string) (requestInput()->bodyAll()['ldap_username'] ?? ''));
|
||||
$ldapPassword = (string) (requestInput()->bodyAll()['password'] ?? '');
|
||||
$remember = !empty(requestInput()->bodyAll()['remember']);
|
||||
|
||||
$ldapAuthService = $authServicesFactory->createLdapAuthService();
|
||||
$ssoUserLinkService = $authServicesFactory->createSsoUserLinkService();
|
||||
|
||||
$tenantRecord = app(\MintyPHP\Service\Auth\AuthGatewayFactory::class)
|
||||
->createAuthTenantGateway()
|
||||
->findById($selectedTenantId);
|
||||
if (!$tenantRecord) {
|
||||
$setLoginWarning(t('We could not continue with these login details'));
|
||||
} else {
|
||||
$ldapConfigResult = $tenantSsoService->getEffectiveLdapProviderConfig($tenantRecord);
|
||||
if (!($ldapConfigResult['ok'] ?? false)) {
|
||||
$setLoginWarning(t('LDAP login is not available for this tenant.'));
|
||||
} else {
|
||||
$ldapConfig = $ldapConfigResult['config'];
|
||||
$allowedDomains = trim((string) ($ldapConfig['allowed_domains'] ?? ''));
|
||||
if ($allowedDomains !== '' && $email !== '') {
|
||||
$domainList = array_filter(array_map('trim', explode(',', $allowedDomains)));
|
||||
if (!$tenantSsoService->isEmailDomainAllowed($email, $domainList)) {
|
||||
$setLoginWarning(t('Your email domain is not allowed for LDAP login on this tenant.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$ldapResult = $ldapAuthService->authenticate($ldapConfig, $ldapUsername !== '' ? $ldapUsername : $email, $ldapPassword);
|
||||
if (!($ldapResult['ok'] ?? false)) {
|
||||
$ldapError = $ldapResult['error'] ?? 'unknown';
|
||||
if ($ldapError === 'credentials_empty') {
|
||||
$setLoginWarning(t('Please enter your username and password.'));
|
||||
} elseif ($ldapError === 'connection_failed') {
|
||||
$setLoginWarning(t('LDAP server is not reachable. Please try again later.'));
|
||||
} else {
|
||||
$setLoginWarning(t('Username or password not valid'));
|
||||
}
|
||||
$passwordFailureResult = $rateLimiterService->registerFailure(
|
||||
$loginPasswordScope,
|
||||
$passwordAttemptKey,
|
||||
$loginPasswordLimit,
|
||||
$loginPasswordWindow,
|
||||
$loginPasswordBlock
|
||||
);
|
||||
if (!($passwordFailureResult['allowed'] ?? true)) {
|
||||
$setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock));
|
||||
}
|
||||
} else {
|
||||
$linkResult = $ssoUserLinkService->linkOrProvisionLdapUser($tenantRecord, $ldapResult, $ldapConfig);
|
||||
if (!($linkResult['ok'] ?? false)) {
|
||||
$linkError = $linkResult['error'] ?? 'unknown';
|
||||
if ($linkError === 'user_inactive') {
|
||||
$setLoginWarning(t('Your account is deactivated.'));
|
||||
} elseif ($linkError === 'email_missing') {
|
||||
$setLoginWarning(t('Your LDAP account does not have an email address configured.'));
|
||||
} else {
|
||||
$setLoginWarning(t('Login could not be completed. Please contact your administrator.'));
|
||||
}
|
||||
} else {
|
||||
$ldapUserId = (int) ($linkResult['user_id'] ?? 0);
|
||||
$loginResult = $authService->loginUserById($ldapUserId, $selectedTenantId, 'ldap');
|
||||
if (!($loginResult['ok'] ?? false)) {
|
||||
$setLoginWarning(t('Login could not be completed. Please contact your administrator.'));
|
||||
} else {
|
||||
$rateLimiterService->reset($loginPasswordScope, $passwordAttemptKey);
|
||||
if ($remember || $tenantSsoService->shouldAutoRememberOnLdapLogin($selectedTenantId)) {
|
||||
$rememberMeService->rememberUser($ldapUserId);
|
||||
}
|
||||
$sessionStore->set('session_started_at', time());
|
||||
$sessionStore->set('session_last_activity', time());
|
||||
Router::redirect($intendedUrlService->retrieveAndForget($sessionStore));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($postedStage === 'login_password') {
|
||||
$stage = 'methods';
|
||||
$passwordAttemptKey = strtolower(trim($email)) . '|' . $clientIp;
|
||||
|
||||
Reference in New Issue
Block a user