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:
@@ -678,6 +678,245 @@ $openOverrideCard = $detailsOpenAll;
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<?php
|
||||
// ── LDAP configuration section ────────────────────────────────
|
||||
$ldapUiState = is_array($ldapUiState ?? null) ? $ldapUiState : [];
|
||||
$ldapEnabled = !empty($values['enabled'] ?? false) ? false : (!empty($values['ldap_enabled'] ?? $ldapConfig['enabled'] ?? false));
|
||||
if (isset($values['ldap_enabled'])) {
|
||||
$ldapEnabled = in_array(strtolower(trim((string) $values['ldap_enabled'])), ['1', 'true', 'yes', 'on'], true);
|
||||
} elseif (isset($ldapConfig['enabled'])) {
|
||||
$ldapEnabled = !empty($ldapConfig['enabled']);
|
||||
}
|
||||
$ldapEnforce = !empty($values['enforce_ldap_login'] ?? $ldapConfig['enforce_ldap_login'] ?? false);
|
||||
$ldapHost = trim((string) ($values['ldap_host'] ?? $values['host'] ?? $ldapConfig['host'] ?? ''));
|
||||
$ldapPort = (int) ($values['ldap_port'] ?? $values['port'] ?? $ldapConfig['port'] ?? 389);
|
||||
$ldapEncryptionMode = (string) ($values['ldap_encryption_mode'] ?? $values['encryption_mode'] ?? $ldapConfig['encryption_mode'] ?? 'starttls');
|
||||
$ldapVerifyTls = !array_key_exists('verify_tls_certificate', ($ldapConfig ?? [])) || !empty($values['ldap_verify_tls_certificate'] ?? $ldapConfig['verify_tls_certificate'] ?? true);
|
||||
$ldapBaseDn = trim((string) ($values['ldap_base_dn'] ?? $values['base_dn'] ?? $ldapConfig['base_dn'] ?? ''));
|
||||
$ldapUserSearchFilter = trim((string) ($values['ldap_user_search_filter'] ?? $values['user_search_filter'] ?? $ldapConfig['user_search_filter'] ?? '(&(objectClass=user)(sAMAccountName=%s))'));
|
||||
$ldapUserSearchScope = (string) ($values['ldap_user_search_scope'] ?? $values['user_search_scope'] ?? $ldapConfig['user_search_scope'] ?? 'sub');
|
||||
$ldapBindMethod = (string) ($values['ldap_bind_method'] ?? $values['bind_method'] ?? $ldapConfig['bind_method'] ?? 'search_then_bind');
|
||||
$ldapBindUsernameFormat = trim((string) ($values['ldap_bind_username_format'] ?? $values['bind_username_format'] ?? $ldapConfig['bind_username_format'] ?? ''));
|
||||
$ldapUniqueIdAttribute = trim((string) ($values['ldap_unique_id_attribute'] ?? $values['unique_id_attribute'] ?? $ldapConfig['unique_id_attribute'] ?? 'objectGUID'));
|
||||
$ldapAttributeMap = $ldapConfig['attribute_map'] ?? [];
|
||||
if (is_string($ldapAttributeMap)) {
|
||||
$ldapAttributeMap = json_decode($ldapAttributeMap, true) ?: [];
|
||||
}
|
||||
$ldapSyncProfileOnLogin = !empty($values['ldap_sync_profile_on_login'] ?? $ldapConfig['sync_profile_on_login'] ?? false);
|
||||
$ldapSyncFieldOptions = $tenantSsoService->ldapProfileSyncFieldOptions();
|
||||
$ldapSyncProfileFields = $tenantSsoService->normalizeLdapProfileSyncFields($values['ldap_sync_profile_fields'] ?? $ldapConfig['sync_profile_fields_list'] ?? []);
|
||||
$ldapAllowedDomains = trim((string) ($values['ldap_allowed_domains'] ?? $ldapConfig['allowed_domains'] ?? ''));
|
||||
$ldapNetworkTimeout = (int) ($values['ldap_network_timeout'] ?? $ldapConfig['network_timeout'] ?? 5);
|
||||
$ldapConfigComplete = !empty($ldapUiState['config_complete']);
|
||||
$ldapConfigErrorLabel = trim((string) ($ldapUiState['config_error_label'] ?? ''));
|
||||
$openLdapSetupCard = $detailsOpenAll || !$ldapEnabled || ($ldapEnabled && !$ldapConfigComplete);
|
||||
?>
|
||||
<hr>
|
||||
<h4><?php e(t('LDAP Authentication')); ?></h4>
|
||||
|
||||
<div class="app-form-info-tiles">
|
||||
<?php
|
||||
$infoTileLabel = t('LDAP login');
|
||||
$infoTileValue = $ldapEnabled ? t('Active') : t('Inactive');
|
||||
$infoTileTone = $ldapEnabled ? 'success' : 'neutral';
|
||||
require templatePath('partials/app-form-info-tile.phtml');
|
||||
$infoTileLabel = t('Configuration complete');
|
||||
$infoTileValue = $ldapConfigComplete ? t('Yes') : t('No');
|
||||
$infoTileTone = $ldapConfigComplete ? 'success' : 'warning';
|
||||
require templatePath('partials/app-form-info-tile.phtml');
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php if ($ldapEnabled && !$ldapConfigComplete && $ldapConfigErrorLabel !== ''): ?>
|
||||
<blockquote data-variant="warning">
|
||||
<strong><?php e(t('LDAP setup is incomplete')); ?></strong><br>
|
||||
<?php e($ldapConfigErrorLabel); ?>
|
||||
</blockquote>
|
||||
<?php endif; ?>
|
||||
|
||||
<details class="app-details-card" name="tenant-ldap-setup" <?php e($openLdapSetupCard ? 'open' : ''); ?>>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('LDAP connection settings')); ?></span>
|
||||
<span class="app-details-card-summary-meta">
|
||||
<span class="badge" data-variant="<?php e($ldapEnabled ? 'success' : 'neutral'); ?>">
|
||||
<?php e(t('LDAP login')); ?>: <?php e($ldapEnabled ? t('Active') : t('Inactive')); ?>
|
||||
</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="ldap_enabled" value="1"
|
||||
<?php e($ldapEnabled ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Enable LDAP login for this tenant')); ?></span>
|
||||
</label>
|
||||
<hr>
|
||||
<div class="grid grid-1-1">
|
||||
<label>
|
||||
<span><?php e(t('LDAP Host')); ?></span>
|
||||
<input type="text" name="ldap_host" value="<?php e($ldapHost); ?>"
|
||||
placeholder="ldap.example.com" <?php e($readonlyAttr); ?>>
|
||||
</label>
|
||||
<label>
|
||||
<span><?php e(t('Port')); ?></span>
|
||||
<input type="number" name="ldap_port" value="<?php e((string) $ldapPort); ?>"
|
||||
min="1" max="65535" <?php e($readonlyAttr); ?>>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-1-1">
|
||||
<label>
|
||||
<span><?php e(t('Encryption')); ?></span>
|
||||
<select name="ldap_encryption_mode" <?php e($disabledAttr); ?>>
|
||||
<option value="starttls" <?php e($ldapEncryptionMode === 'starttls' ? 'selected' : ''); ?>>STARTTLS</option>
|
||||
<option value="ldaps" <?php e($ldapEncryptionMode === 'ldaps' ? 'selected' : ''); ?>>LDAPS (SSL)</option>
|
||||
<option value="none" <?php e($ldapEncryptionMode === 'none' ? 'selected' : ''); ?>><?php e(t('None (not recommended)')); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" name="ldap_verify_tls_certificate" value="1"
|
||||
<?php e($ldapVerifyTls ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Verify TLS certificate')); ?></span>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span><?php e(t('Base DN')); ?></span>
|
||||
<input type="text" name="ldap_base_dn" value="<?php e($ldapBaseDn); ?>"
|
||||
placeholder="DC=corp,DC=example,DC=com" <?php e($readonlyAttr); ?>>
|
||||
</label>
|
||||
<hr>
|
||||
<label>
|
||||
<span><?php e(t('Bind DN (service account)')); ?></span>
|
||||
<input type="text" name="ldap_bind_dn" value="" autocomplete="off"
|
||||
placeholder="CN=svc-ldap,OU=Service Accounts,DC=corp,DC=example,DC=com" <?php e($readonlyAttr); ?>>
|
||||
<small class="muted"><?php e(t('Write-only. Leave empty to keep current value.')); ?></small>
|
||||
</label>
|
||||
<label>
|
||||
<span><?php e(t('Bind password')); ?></span>
|
||||
<input type="password" name="ldap_bind_password" value="" autocomplete="new-password" <?php e($readonlyAttr); ?>>
|
||||
<small class="muted"><?php e(t('Write-only. Leave empty to keep current value.')); ?></small>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" name="clear_ldap_bind_password" value="1" <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Clear bind password')); ?></span>
|
||||
</label>
|
||||
<hr>
|
||||
<p>
|
||||
<button type="button" class="secondary outline small" id="ldap-test-connection-button"
|
||||
data-ldap-test-url="<?php e(lurl('admin/tenants/ldap-test-connection')); ?>"
|
||||
data-tenant-id="<?php e((string) $tenantId); ?>">
|
||||
<i class="bi bi-plug" aria-hidden="true"></i> <?php e(t('Test connection')); ?>
|
||||
</button>
|
||||
<span id="ldap-test-connection-result" aria-live="polite"></span>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="tenant-ldap-search" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Search and bind settings')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<label>
|
||||
<span><?php e(t('Bind method')); ?></span>
|
||||
<select name="ldap_bind_method" <?php e($disabledAttr); ?>>
|
||||
<option value="search_then_bind" <?php e($ldapBindMethod === 'search_then_bind' ? 'selected' : ''); ?>><?php e(t('Search then bind (recommended)')); ?></option>
|
||||
<option value="direct_bind" <?php e($ldapBindMethod === 'direct_bind' ? 'selected' : ''); ?>><?php e(t('Direct bind')); ?></option>
|
||||
</select>
|
||||
<small class="muted"><?php e(t('Search-then-bind uses a service account to find the user DN first, then binds with the user credentials.')); ?></small>
|
||||
</label>
|
||||
<label>
|
||||
<span><?php e(t('User search filter')); ?></span>
|
||||
<input type="text" name="ldap_user_search_filter" value="<?php e($ldapUserSearchFilter); ?>"
|
||||
placeholder="(&(objectClass=user)(sAMAccountName=%s))" <?php e($readonlyAttr); ?>>
|
||||
<small class="muted"><?php e(t('Use %s as placeholder for the username. Examples: (&(objectClass=user)(sAMAccountName=%s)) for AD, (&(objectClass=inetOrgPerson)(uid=%s)) for OpenLDAP')); ?></small>
|
||||
</label>
|
||||
<div class="grid grid-1-1">
|
||||
<label>
|
||||
<span><?php e(t('Search scope')); ?></span>
|
||||
<select name="ldap_user_search_scope" <?php e($disabledAttr); ?>>
|
||||
<option value="sub" <?php e($ldapUserSearchScope === 'sub' ? 'selected' : ''); ?>><?php e(t('Subtree (recursive)')); ?></option>
|
||||
<option value="one" <?php e($ldapUserSearchScope === 'one' ? 'selected' : ''); ?>><?php e(t('One level')); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span><?php e(t('Network timeout (seconds)')); ?></span>
|
||||
<input type="number" name="ldap_network_timeout" value="<?php e((string) $ldapNetworkTimeout); ?>"
|
||||
min="1" max="30" <?php e($readonlyAttr); ?>>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span><?php e(t('Direct bind username format')); ?></span>
|
||||
<input type="text" name="ldap_bind_username_format" value="<?php e($ldapBindUsernameFormat); ?>"
|
||||
placeholder="%s@corp.example.com" <?php e($readonlyAttr); ?>>
|
||||
<small class="muted"><?php e(t('Only for direct bind. Use %s as placeholder. Example: %s@corp.example.com or uid=%s,ou=People,dc=example,dc=com')); ?></small>
|
||||
</label>
|
||||
<label>
|
||||
<span><?php e(t('Unique ID attribute')); ?></span>
|
||||
<input type="text" name="ldap_unique_id_attribute" value="<?php e($ldapUniqueIdAttribute); ?>"
|
||||
placeholder="objectGUID" <?php e($readonlyAttr); ?>>
|
||||
<small class="muted"><?php e(t('AD: objectGUID, OpenLDAP: entryUUID, FreeIPA: ipaUniqueID')); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="app-details-card" name="tenant-ldap-mapping" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Attribute mapping and sync')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<small class="muted"><?php e(t('Map application fields to LDAP directory attributes.')); ?></small>
|
||||
<div class="grid grid-1-1">
|
||||
<?php
|
||||
$defaultMap = [
|
||||
'first_name' => 'givenName',
|
||||
'last_name' => 'sn',
|
||||
'email' => 'mail',
|
||||
'phone' => 'telephoneNumber',
|
||||
'mobile' => 'mobile',
|
||||
];
|
||||
foreach ($defaultMap as $appField => $defaultLdapAttr):
|
||||
$currentLdapAttr = $ldapAttributeMap[$appField] ?? $defaultLdapAttr;
|
||||
?>
|
||||
<label>
|
||||
<span><?php e(t(ucfirst(str_replace('_', ' ', $appField)))); ?> →</span>
|
||||
<input type="text" name="ldap_attribute_map[<?php e($appField); ?>]"
|
||||
value="<?php e($currentLdapAttr); ?>" <?php e($readonlyAttr); ?>>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<hr>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="ldap_sync_profile_on_login" value="1"
|
||||
<?php e($ldapSyncProfileOnLogin ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Profile sync on LDAP login')); ?></span>
|
||||
</label>
|
||||
<small class="muted"><?php e(t('User profile fields are global and affect all tenants of this user')); ?></small>
|
||||
<div class="grid grid-1-1">
|
||||
<?php foreach ($ldapSyncFieldOptions as $syncFieldKey => $syncFieldLabel): ?>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" name="ldap_sync_profile_fields[]"
|
||||
value="<?php e($syncFieldKey); ?>"
|
||||
<?php e(in_array($syncFieldKey, $ldapSyncProfileFields, true) ? 'checked' : ''); ?>
|
||||
<?php e($disabledAttr); ?>>
|
||||
<span><?php e(t($syncFieldLabel)); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<hr>
|
||||
<label class="app-field">
|
||||
<input type="checkbox" role="switch" name="enforce_ldap_login" value="1"
|
||||
<?php e($ldapEnforce ? 'checked' : ''); ?> <?php e($disabledAttr); ?>>
|
||||
<span><?php e(t('Allow LDAP-only login (disable local password login)')); ?></span>
|
||||
</label>
|
||||
<small class="muted"><?php e(t('LDAP-only login requires a complete configuration')); ?></small>
|
||||
<hr>
|
||||
<label>
|
||||
<span><?php e(t('Allowed email domains')); ?></span>
|
||||
<textarea name="ldap_allowed_domains" rows="2" placeholder="example.com" <?php e($readonlyAttr); ?>><?php e($ldapAllowedDomains); ?></textarea>
|
||||
<small><?php e(t('Optional allow-list, comma or newline separated.')); ?></small>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
@@ -67,11 +67,13 @@ $customFieldDefinitions = $canManageCustomFields
|
||||
: [];
|
||||
$ssoConfig = $canManageSso ? $tenantSsoService->getTenantMicrosoftAuth($tenantId) : [];
|
||||
$ssoUiState = $canManageSso ? $tenantSsoService->buildMicrosoftUiState($tenantId, $ssoConfig) : [];
|
||||
$ldapConfig = $canManageSso ? $tenantSsoService->getTenantLdapAuth($tenantId) : [];
|
||||
$ldapUiState = $canManageSso ? $tenantSsoService->buildLdapUiState($tenantId) : [];
|
||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($tenant, ['created_by', 'modified_by', 'status_changed_by']);
|
||||
|
||||
$errorBag = formErrors();
|
||||
$errors = [];
|
||||
$form = array_merge($tenant, $ssoConfig);
|
||||
$form = array_merge($tenant, $ssoConfig, $ldapConfig);
|
||||
|
||||
if ($request->isMethod('POST') && !Session::checkCsrfToken()) {
|
||||
Flash::error(t('Form expired, please try again'), $editTarget, 'csrf_expired');
|
||||
@@ -119,6 +121,10 @@ if ($request->isMethod('POST')) {
|
||||
if (!($ssoSaveResult['ok'] ?? false)) {
|
||||
$errorBag->merge($ssoSaveResult['errors'] ?? [t('Tenant SSO settings could not be saved')]);
|
||||
}
|
||||
$ldapSaveResult = $tenantSsoService->saveTenantLdapAuth($tenantId, $post);
|
||||
if (!($ldapSaveResult['ok'] ?? false)) {
|
||||
$errorBag->merge($ldapSaveResult['errors'] ?? [t('Tenant LDAP settings could not be saved')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,12 +155,30 @@ if ($request->isMethod('POST')) {
|
||||
'auto_remember_mode' => $post['microsoft_auto_remember_mode'] ?? null,
|
||||
]);
|
||||
$ssoUiState = $tenantSsoService->buildMicrosoftUiState($tenantId, $post);
|
||||
$ldapUiState = $tenantSsoService->buildLdapUiState($tenantId, $post);
|
||||
$form = array_merge($form, [
|
||||
'ldap_enabled' => !empty($post['ldap_enabled']) ? '1' : '0',
|
||||
'enforce_ldap_login' => !empty($post['enforce_ldap_login']) ? '1' : '0',
|
||||
'ldap_host' => trim((string) ($post['ldap_host'] ?? '')),
|
||||
'ldap_port' => (int) ($post['ldap_port'] ?? 389),
|
||||
'ldap_encryption_mode' => (string) ($post['ldap_encryption_mode'] ?? 'starttls'),
|
||||
'ldap_verify_tls_certificate' => !empty($post['ldap_verify_tls_certificate']) ? '1' : '0',
|
||||
'ldap_base_dn' => trim((string) ($post['ldap_base_dn'] ?? '')),
|
||||
'ldap_user_search_filter' => trim((string) ($post['ldap_user_search_filter'] ?? '')),
|
||||
'ldap_bind_method' => (string) ($post['ldap_bind_method'] ?? 'search_then_bind'),
|
||||
'ldap_bind_username_format' => trim((string) ($post['ldap_bind_username_format'] ?? '')),
|
||||
'ldap_unique_id_attribute' => trim((string) ($post['ldap_unique_id_attribute'] ?? 'objectGUID')),
|
||||
'ldap_sync_profile_on_login' => !empty($post['ldap_sync_profile_on_login']) ? '1' : '0',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($canManageSso && empty($ssoUiState)) {
|
||||
$ssoUiState = $tenantSsoService->buildMicrosoftUiState($tenantId, $form);
|
||||
}
|
||||
if ($canManageSso && empty($ldapUiState)) {
|
||||
$ldapUiState = $tenantSsoService->buildLdapUiState($tenantId);
|
||||
}
|
||||
|
||||
$validationSummaryErrors = $errorBag->toArray();
|
||||
$errors = $errorBag->toFlatList();
|
||||
|
||||
75
pages/admin/tenants/ldap-test-connection/data().php
Normal file
75
pages/admin/tenants/ldap-test-connection/data().php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (requestInput()->method() !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Session::checkCsrfToken()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'CSRF token invalid']);
|
||||
return;
|
||||
}
|
||||
|
||||
$session = app(\MintyPHP\Http\SessionStoreInterface::class)->all();
|
||||
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||
$decision = app(\MintyPHP\Service\Access\AuthorizationService::class)->authorize(
|
||||
TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT,
|
||||
[
|
||||
'actor_user_id' => $currentUserId,
|
||||
'target_tenant_id' => (int) (requestInput()->bodyAll()['tenant_id'] ?? 0),
|
||||
]
|
||||
);
|
||||
if (!$decision->isAllowed()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Forbidden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$capabilities = $decision->attribute('capabilities', []);
|
||||
if (!is_array($capabilities) || empty($capabilities['can_manage_sso'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Forbidden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$post = requestInput()->bodyAll();
|
||||
$host = trim((string) ($post['ldap_host'] ?? ''));
|
||||
$port = (int) ($post['ldap_port'] ?? 389);
|
||||
$encryptionMode = (string) ($post['ldap_encryption_mode'] ?? 'starttls');
|
||||
$verifyTls = !empty($post['ldap_verify_tls_certificate']);
|
||||
$baseDn = trim((string) ($post['ldap_base_dn'] ?? ''));
|
||||
$bindDn = trim((string) ($post['ldap_bind_dn'] ?? ''));
|
||||
$bindPassword = trim((string) ($post['ldap_bind_password'] ?? ''));
|
||||
$networkTimeout = max(1, min(10, (int) ($post['ldap_network_timeout'] ?? 5)));
|
||||
|
||||
if ($host === '') {
|
||||
echo json_encode(['ok' => false, 'error' => t('LDAP host is required')]);
|
||||
return;
|
||||
}
|
||||
|
||||
$ldapAuthService = app(\MintyPHP\Service\Auth\LdapAuthService::class);
|
||||
|
||||
$config = [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'encryption_mode' => $encryptionMode,
|
||||
'verify_tls_certificate' => $verifyTls,
|
||||
'base_dn' => $baseDn,
|
||||
'bind_dn' => $bindDn,
|
||||
'bind_password' => $bindPassword,
|
||||
'network_timeout' => $networkTimeout,
|
||||
];
|
||||
|
||||
$result = $ldapAuthService->testConnection($config);
|
||||
|
||||
echo json_encode($result);
|
||||
@@ -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;
|
||||
|
||||
@@ -22,7 +22,7 @@ $tenantHint = trim((string) ($tenantHint ?? ''));
|
||||
$tenantCandidates = is_array($tenantCandidates ?? null) ? $tenantCandidates : [];
|
||||
$selectedTenantId = (int) ($selectedTenantId ?? 0);
|
||||
$selectedTenant = is_array($selectedTenant ?? null) ? $selectedTenant : null;
|
||||
$selectedTenantMethods = is_array($selectedTenantMethods ?? null) ? $selectedTenantMethods : ['local' => false, 'microsoft' => false];
|
||||
$selectedTenantMethods = is_array($selectedTenantMethods ?? null) ? $selectedTenantMethods : ['local' => false, 'microsoft' => false, 'ldap' => false];
|
||||
$selectedTenantSlug = (string) ($selectedTenant['slug'] ?? '');
|
||||
$selectedTenantLabel = trim((string) ($selectedTenant['description'] ?? ''));
|
||||
if ($selectedTenantLabel === '') {
|
||||
@@ -183,7 +183,46 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($selectedTenantMethods['local']) && !empty($selectedTenantMethods['microsoft'])): ?>
|
||||
<?php if (!empty($selectedTenantMethods['ldap'])): ?>
|
||||
<?php
|
||||
$hasMethodAbove = !empty($selectedTenantMethods['microsoft']);
|
||||
?>
|
||||
<?php if ($hasMethodAbove): ?>
|
||||
<div class="login-alt-separator" aria-hidden="true">
|
||||
<span><?php e(t('Or')); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="post" class="login-ldap-form" data-login-submit-lock="1">
|
||||
<input type="hidden" name="stage" value="login_ldap">
|
||||
<input type="hidden" name="email" value="<?php e($email); ?>">
|
||||
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
||||
<input type="hidden" name="tenant_id" value="<?php e((string) $selectedTenantId); ?>">
|
||||
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
||||
<label for="ldap_username">
|
||||
<span><?php e(t('LDAP Username')); ?></span>
|
||||
<input required type="text" name="ldap_username" id="ldap_username" autocomplete="username" />
|
||||
</label>
|
||||
<label for="ldap_password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input required type="password" name="password" id="ldap_password" autocomplete="current-password" <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<p>
|
||||
<label class="app-field inline">
|
||||
<input type="checkbox" name="remember" value="1">
|
||||
<span><?php e(t('Remember me')); ?></span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit"><i class="bi bi-shield-lock" aria-hidden="true"></i> <?php e(t('Sign in with LDAP')); ?></button>
|
||||
</p>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$hasMethodAboveLocal = !empty($selectedTenantMethods['microsoft']) || !empty($selectedTenantMethods['ldap']);
|
||||
?>
|
||||
<?php if (!empty($selectedTenantMethods['local']) && $hasMethodAboveLocal): ?>
|
||||
<div class="login-alt-separator" aria-hidden="true">
|
||||
<span><?php e(t('Or')); ?></span>
|
||||
</div>
|
||||
@@ -214,7 +253,7 @@ $returnToSanitized = (string) ($returnToSanitized ?? '');
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($selectedTenantMethods['local']) && empty($selectedTenantMethods['microsoft'])): ?>
|
||||
<?php if (empty($selectedTenantMethods['local']) && empty($selectedTenantMethods['microsoft']) && empty($selectedTenantMethods['ldap'])): ?>
|
||||
<div class="notice" data-variant="warning" role="status" aria-live="polite">
|
||||
<?php e(t('No login method is available for this tenant')); ?>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user