forked from fa/breadcrumb-the-shire
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>
271 lines
14 KiB
PHTML
271 lines
14 KiB
PHTML
<?php
|
|
|
|
/**
|
|
* @var string $stage
|
|
* @var string $email
|
|
* @var string $tenantHint
|
|
* @var array $tenantCandidates
|
|
* @var int $selectedTenantId
|
|
* @var array|null $selectedTenant
|
|
* @var array $selectedTenantMethods
|
|
* @var array<int, string> $errors
|
|
*/
|
|
|
|
use MintyPHP\Buffer;
|
|
use MintyPHP\Session;
|
|
|
|
Buffer::set('title', t('Login'));
|
|
|
|
$stage = (string) ($stage ?? 'resolve_email');
|
|
$email = trim((string) ($email ?? ''));
|
|
$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, 'ldap' => false];
|
|
$selectedTenantSlug = (string) ($selectedTenant['slug'] ?? '');
|
|
$selectedTenantLabel = trim((string) ($selectedTenant['description'] ?? ''));
|
|
if ($selectedTenantLabel === '') {
|
|
$selectedTenantLabel = t('Select tenant');
|
|
}
|
|
$selectedTenantAvatarUrl = (string) ($selectedTenant['avatar_url'] ?? '');
|
|
$selectedTenantInitial = strtoupper(substr($selectedTenantLabel, 0, 1));
|
|
if ($selectedTenantInitial === '') {
|
|
$selectedTenantInitial = '?';
|
|
}
|
|
$canSwitchTenant = count($tenantCandidates) > 1;
|
|
$errors = is_array($errors ?? null) ? $errors : [];
|
|
$errorNoticeId = $errors ? 'auth-login-error-notice' : '';
|
|
$loginHeading = t('Login');
|
|
$loginSubheading = t('Login credentials');
|
|
$authHelpContext = ['show_support' => true];
|
|
if ($stage === 'resolve_email') {
|
|
$authHelpContext = [
|
|
'show_register' => true,
|
|
'show_support' => true,
|
|
];
|
|
} elseif ($stage === 'choose_tenant') {
|
|
$authHelpContext = [
|
|
'show_support' => true,
|
|
];
|
|
} else {
|
|
$authHelpContext = [
|
|
'show_forgot' => !empty($selectedTenantMethods['local']),
|
|
'show_support' => true,
|
|
];
|
|
}
|
|
$authLogoHref = lurl('login');
|
|
if ($tenantHint !== '') {
|
|
$authLogoHref .= '?tenant=' . rawurlencode($tenantHint);
|
|
}
|
|
$returnToSanitized = (string) ($returnToSanitized ?? '');
|
|
|
|
?>
|
|
|
|
<article class="login-card">
|
|
<?php require templatePath('partials/auth-logo.phtml'); ?>
|
|
<header>
|
|
<hgroup>
|
|
<h1><?php e($loginHeading); ?></h1>
|
|
<p><?php e($loginSubheading); ?></p>
|
|
</hgroup>
|
|
</header>
|
|
|
|
|
|
<?php if ($errors): ?>
|
|
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
|
<ul>
|
|
<?php foreach ($errors as $error): ?>
|
|
<li><?php e($error); ?></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($stage === 'resolve_email'): ?>
|
|
<form method="post" data-login-submit-lock="1">
|
|
<input type="hidden" name="stage" value="resolve_email">
|
|
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
|
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
|
<label for="email">
|
|
<span><?php e(t('Email')); ?></span>
|
|
<input required type="email" name="email" id="email" value="<?php e($email); ?>" autocomplete="email" autofocus <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
|
</label>
|
|
<p>
|
|
<button type="submit"><?php e(t('Continue')); ?></button>
|
|
</p>
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
<?php elseif ($stage === 'choose_tenant'): ?>
|
|
<form method="post" class="login-tenant-select-form" data-login-submit-lock="1">
|
|
<input type="hidden" name="stage" value="choose_tenant">
|
|
<input type="hidden" name="email" value="<?php e($email); ?>">
|
|
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
|
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
|
<fieldset class="login-tenant-fieldset">
|
|
<legend class="login-tenant-select-label"><?php e(t('Select tenant')); ?></legend>
|
|
<div class="login-tenant-choice-grid">
|
|
<?php foreach ($tenantCandidates as $tenantCandidate): ?>
|
|
<?php
|
|
$tenantId = (int) ($tenantCandidate['id'] ?? 0);
|
|
if ($tenantId <= 0) {
|
|
continue;
|
|
}
|
|
$tenantLabel = trim((string) ($tenantCandidate['description'] ?? ''));
|
|
if ($tenantLabel === '') {
|
|
$tenantLabel = t('Select tenant');
|
|
}
|
|
$tenantAvatarUrl = (string) ($tenantCandidate['avatar_url'] ?? '');
|
|
$tenantInitial = strtoupper(substr($tenantLabel, 0, 1));
|
|
if ($tenantInitial === '') {
|
|
$tenantInitial = '?';
|
|
}
|
|
?>
|
|
<label class="app-field login-tenant-choice" data-tooltip="<?php e($tenantLabel); ?>" data-tooltip-pos="top">
|
|
<input type="radio" name="tenant_id" value="<?php e((string) $tenantId); ?>" <?php e($tenantId === $selectedTenantId ? 'checked autofocus' : ''); ?>>
|
|
<span class="login-tenant-choice-body">
|
|
<?php if ($tenantAvatarUrl !== ''): ?>
|
|
<img class="login-tenant-choice-avatar" src="<?php e($tenantAvatarUrl); ?>" alt="" loading="lazy">
|
|
<?php else: ?>
|
|
<span class="login-tenant-choice-avatar-placeholder"><?php e($tenantInitial); ?></span>
|
|
<?php endif; ?>
|
|
<span class="login-tenant-choice-text"><?php e($tenantLabel); ?></span>
|
|
</span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</fieldset>
|
|
<p>
|
|
<button type="submit"><?php e(t('Continue')); ?></button>
|
|
</p>
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
<form method="post">
|
|
<input type="hidden" name="stage" value="reset">
|
|
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
|
<p>
|
|
<button type="submit" class="secondary outline"><?php e(t('Use different email')); ?></button>
|
|
</p>
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
<?php else: ?>
|
|
<?php $hasSelectedTenantAvatar = $selectedTenantAvatarUrl !== ''; ?>
|
|
<?php if ($canSwitchTenant): ?>
|
|
<section class="login-tenant-context" role="status" aria-live="polite">
|
|
<div class="login-tenant-context-main">
|
|
<?php if ($hasSelectedTenantAvatar): ?>
|
|
<img class="login-tenant-context-avatar" src="<?php e($selectedTenantAvatarUrl); ?>" alt="" loading="lazy">
|
|
<?php else: ?>
|
|
<span class="login-tenant-context-avatar-placeholder login-tenant-context-avatar-placeholder--name"><?php e($selectedTenantLabel); ?></span>
|
|
<?php endif; ?>
|
|
<?php if ($hasSelectedTenantAvatar): ?>
|
|
<div class="login-tenant-context-text">
|
|
<p class="login-tenant-context-name"><?php e($selectedTenantLabel); ?></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<form method="post" class="login-tenant-context-form">
|
|
<input type="hidden" name="stage" value="resolve_email">
|
|
<input type="hidden" name="email" value="<?php e($email); ?>">
|
|
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
|
<?php if ($returnToSanitized !== ''): ?><input type="hidden" name="return_to" value="<?php e($returnToSanitized); ?>"><?php endif; ?>
|
|
<button type="submit" class="secondary outline small"><?php e(t('Switch tenant')); ?></button>
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<div class="login-methods-stack">
|
|
<?php if (!empty($selectedTenantMethods['microsoft']) && $selectedTenantSlug !== ''): ?>
|
|
<a class="primary login-microsoft-link"
|
|
href="<?php e(lurl('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug))); ?>">
|
|
<i class="bi bi-microsoft" aria-hidden="true"></i> <?php e(t('Sign in with Microsoft')); ?>
|
|
</a>
|
|
<?php endif; ?>
|
|
|
|
<?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>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($selectedTenantMethods['local'])): ?>
|
|
<form method="post" class="login-password-form" data-login-submit-lock="1">
|
|
<input type="hidden" name="stage" value="login_password">
|
|
<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="password">
|
|
<span><?php e(t('Password')); ?></span>
|
|
<input required type="password" name="password" id="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"><?php e(t('Login')); ?></button>
|
|
</p>
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?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>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
|
|
<?php require templatePath('partials/auth-help-links.phtml'); ?>
|
|
<hr>
|
|
<p class="login-security-note">
|
|
<i class="bi bi-lock-fill" aria-hidden="true"></i>
|
|
<span><?php e(t('Encrypted connection')); ?></span>
|
|
</p>
|
|
<small class="login-security-note-detail muted"><?php e(t('HTTPS/TLS 1.2+ in transit')); ?></small>
|
|
</article>
|