queryAll()['return_to'] ?? '')); if ($returnTo === '') { $returnTo = trim((string) (requestInput()->bodyAll()['return_to'] ?? '')); } if ($returnTo !== '') { $intendedUrlService->store($returnTo, $sessionStore); } // Expose sanitized return_to for hidden form fields so it survives across // POST stages even if the query string or session is lost. $returnToSanitized = $intendedUrlService->sanitize($returnTo !== '' ? $returnTo : ($intendedUrlService->retrieve($sessionStore) ?? '')); $session = $sessionStore->all(); if (!empty($session['user']['id'])) { if (Flash::has()) { Flash::keep(); } Router::redirect($intendedUrlService->retrieveAndForget($sessionStore)); return; } $tenantHint = trim((string) (requestInput()->queryAll()['tenant'] ?? requestInput()->bodyAll()['tenant_hint'] ?? '')); $stage = 'resolve_email'; $email = ''; $tenantCandidates = []; $tenantCandidateById = []; $selectedTenantId = 0; $selectedTenant = null; $selectedTenantMethods = ['local' => false, 'microsoft' => false, 'microsoft_reason' => '', 'ldap' => false, 'ldap_reason' => '']; $errorBag = formErrors(); $setLoginWarning = static function (string $message) use ($errorBag): void { $errorBag->addGlobal($message); }; $setRateLimitWarning = static function (int $retryAfterSeconds) use ($setLoginWarning): void { $retryAfterSeconds = max(1, $retryAfterSeconds); http_response_code(429); header('Retry-After: ' . $retryAfterSeconds); $setLoginWarning(t('Too many login attempts. Please wait and try again.')); }; $rateLimiterService = (app(SecurityServicesFactory::class))->createRateLimiterService(); $loginResolveScope = 'login.resolve.ip'; $loginResolveLimit = 25; $loginResolveWindow = 600; $loginResolveBlock = 300; $loginPasswordScope = 'login.password.email_ip'; $loginPasswordLimit = 5; $loginPasswordWindow = 900; $loginPasswordBlock = 900; $authServicesFactory = app(AuthServicesFactory::class); $authService = $authServicesFactory->createAuthService(); $rememberMeService = $authServicesFactory->createRememberMeService(); $tenantSsoService = $authServicesFactory->createTenantSsoService(); $tenantAvatarService = (app(TenantServicesFactory::class))->createTenantAvatarService(); $userServicesFactory = app(UserServicesFactory::class); $userReadRepository = $userServicesFactory->createUserReadRepository(); $userTenantContextService = $userServicesFactory->createUserTenantContextService(); $resolveLoginCandidates = static function (string $inputEmail) use ($userReadRepository, $userTenantContextService, $tenantSsoService, $tenantAvatarService): array { $emailValue = strtolower(trim($inputEmail)); if ($emailValue === '' || !filter_var($emailValue, FILTER_VALIDATE_EMAIL)) { return ['ok' => false]; } $user = $userReadRepository->findByEmail($emailValue); $isKnownActiveUser = $user && (int) ($user['active'] ?? 0) === 1 && (int) ($user['id'] ?? 0) > 0; $tenants = []; if ($isKnownActiveUser) { $userId = (int) $user['id']; $tenants = $userTenantContextService->getAssignedActiveTenants($userId); } // Fallback: domain-based SSO discovery for users not yet provisioned locally $discovery = false; $discoveryMethodsByTenantId = []; if (!$tenants) { $discoveryResults = $tenantSsoService->discoverTenantsByEmailDomain($emailValue); if (!$discoveryResults) { return ['ok' => false]; } $discovery = true; $tenants = array_map(static fn (array $r): array => $r['tenant'], $discoveryResults); foreach ($discoveryResults as $r) { $discoveryMethodsByTenantId[(int) $r['id']] = $r['methods']; } } $candidates = []; $candidateById = []; foreach ($tenants as $tenant) { $tenantId = (int) ($tenant['id'] ?? 0); if ($tenantId <= 0) { continue; } $tenantSlug = $tenantSsoService->tenantLoginSlug($tenant); if ($tenantSlug === '') { continue; } $methods = $discovery ? ($discoveryMethodsByTenantId[$tenantId] ?? ['local' => false, 'microsoft' => false, 'microsoft_reason' => '', 'ldap' => false, 'ldap_reason' => '']) : $tenantSsoService->resolveTenantLoginMethods($tenantId); $tenantUuid = (string) ($tenant['uuid'] ?? ''); $hasAvatar = $tenantUuid !== '' && $tenantAvatarService->hasAvatar($tenantUuid); $candidate = [ 'id' => $tenantId, 'uuid' => $tenantUuid, 'description' => (string) ($tenant['description'] ?? ''), 'slug' => $tenantSlug, 'has_avatar' => $hasAvatar, 'avatar_url' => $hasAvatar ? lurl('auth/tenant-avatar-file?uuid=' . rawurlencode($tenantUuid) . '&size=256') : '', 'methods' => $methods, ]; $candidates[] = $candidate; $candidateById[$tenantId] = $candidate; } if (!$candidates) { return ['ok' => false]; } return [ 'ok' => true, 'email' => $emailValue, 'user' => $isKnownActiveUser ? $user : null, 'candidates' => $candidates, 'candidate_by_id' => $candidateById, 'discovery' => $discovery, ]; }; $resolveSelectedTenantId = static function (array $candidateByIdValue, int $requestedTenantIdValue, string $tenantHintValue): int { if ($requestedTenantIdValue > 0 && isset($candidateByIdValue[$requestedTenantIdValue])) { return $requestedTenantIdValue; } $hint = trim(strtolower($tenantHintValue)); if ($hint !== '') { foreach ($candidateByIdValue as $candidate) { $candidateSlug = strtolower((string) ($candidate['slug'] ?? '')); if ($candidateSlug !== '' && $candidateSlug === $hint) { return (int) ($candidate['id'] ?? 0); } } } $first = reset($candidateByIdValue); if (!is_array($first)) { return 0; } return (int) ($first['id'] ?? 0); }; if (requestInput()->method() === 'POST') { if (!Session::checkCsrfToken()) { $errorBag->addGlobal(t('Form expired, please try again')); $stage = 'resolve_email'; } else { $postedStage = trim((string) (requestInput()->bodyAll()['stage'] ?? 'resolve_email')); $email = trim((string) (requestInput()->bodyAll()['email'] ?? '')); $clientIp = trim((string) ($requestRuntime->ip())); if ($clientIp === '') { $clientIp = 'unknown'; } if ($postedStage === 'reset') { $email = ''; $stage = 'resolve_email'; } else { if ($postedStage === 'resolve_email') { $resolveLimitResult = $rateLimiterService->isBlocked($loginResolveScope, $clientIp); if (!($resolveLimitResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($resolveLimitResult['retry_after'] ?? 60)); return; } } $resolved = $resolveLoginCandidates($email); if (!$resolved['ok']) { $stage = 'resolve_email'; $setLoginWarning(t('We could not continue with these login details')); if ($postedStage === 'resolve_email') { $resolveFailureResult = $rateLimiterService->registerFailure( $loginResolveScope, $clientIp, $loginResolveLimit, $loginResolveWindow, $loginResolveBlock ); if (!($resolveFailureResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($resolveFailureResult['retry_after'] ?? $loginResolveBlock)); } } } else { $email = (string) $resolved['email']; $tenantCandidates = $resolved['candidates']; $tenantCandidateById = $resolved['candidate_by_id']; $postedTenantId = (int) (requestInput()->bodyAll()['tenant_id'] ?? 0); 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')); $selectedTenantId = $resolveSelectedTenantId( $tenantCandidateById, 0, trim((string) (requestInput()->bodyAll()['tenant_hint'] ?? $tenantHint)) ); $selectedTenant = $tenantCandidateById[$selectedTenantId] ?? null; $selectedTenantMethods = is_array($selectedTenant['methods'] ?? null) ? $selectedTenant['methods'] : $selectedTenantMethods; } else { $selectedTenantId = $resolveSelectedTenantId( $tenantCandidateById, $postedTenantId, trim((string) (requestInput()->bodyAll()['tenant_hint'] ?? $tenantHint)) ); $selectedTenant = $tenantCandidateById[$selectedTenantId] ?? null; if (!is_array($selectedTenant)) { $stage = 'resolve_email'; $setLoginWarning(t('We could not continue with these login details')); } else { $selectedTenantMethods = $selectedTenant['methods']; $selectedTenantSlug = trim((string) $selectedTenant['slug']); $isMicrosoftOnlyTenant = empty($selectedTenantMethods['local']) && !empty($selectedTenantMethods['microsoft']) && empty($selectedTenantMethods['ldap']) && $selectedTenantSlug !== ''; if ( $postedStage === 'resolve_email' && count($tenantCandidates) === 1 && $isMicrosoftOnlyTenant ) { Router::redirect('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug)); return; } if ( $postedStage === 'choose_tenant' && $isMicrosoftOnlyTenant ) { Router::redirect('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug)); return; } if ($postedStage === 'resolve_email') { $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; $passwordLimitResult = $rateLimiterService->isBlocked($loginPasswordScope, $passwordAttemptKey); if (!($passwordLimitResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($passwordLimitResult['retry_after'] ?? 60)); return; } if (empty($selectedTenantMethods['local'])) { $setLoginWarning(t('Password login is disabled for this tenant. Please use Microsoft login.')); } else { $password = (string) (requestInput()->bodyAll()['password'] ?? ''); $remember = !empty(requestInput()->bodyAll()['remember']); $result = $authService->login($email, $password); if (!($result['ok'] ?? false)) { if (!empty($result['needs_verification'])) { $sessionStore->set('email_verification_email', $result['email'] ?? $email); Flash::error(t('Please verify your email first'), 'verify-email', 'login_not_verified'); Router::redirect('verify-email'); return; } $setLoginWarning(t((string) ($result['message'] ?? 'Email/password not valid'))); $passwordFailureResult = $rateLimiterService->registerFailure( $loginPasswordScope, $passwordAttemptKey, $loginPasswordLimit, $loginPasswordWindow, $loginPasswordBlock ); if (!($passwordFailureResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock)); } } else { $authenticatedUser = $sessionStore->get('user', []); $userId = (int) (is_array($authenticatedUser) ? ($authenticatedUser['id'] ?? 0) : 0); if ($userId <= 0 || !$authService->canLoginToTenant($userId, $selectedTenantId)) { $authService->logout(); $setLoginWarning(t('No access to this tenant')); $passwordFailureResult = $rateLimiterService->registerFailure( $loginPasswordScope, $passwordAttemptKey, $loginPasswordLimit, $loginPasswordWindow, $loginPasswordBlock ); if (!($passwordFailureResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock)); } } else { $setTenant = $userTenantContextService->setCurrentTenant($userId, $selectedTenantId); if (!($setTenant['ok'] ?? false)) { $authService->logout(); $setLoginWarning(t('No access to this tenant')); $passwordFailureResult = $rateLimiterService->registerFailure( $loginPasswordScope, $passwordAttemptKey, $loginPasswordLimit, $loginPasswordWindow, $loginPasswordBlock ); if (!($passwordFailureResult['allowed'] ?? true)) { $setRateLimitWarning((int) ($passwordFailureResult['retry_after'] ?? $loginPasswordBlock)); } } else { $rateLimiterService->reset($loginPasswordScope, $passwordAttemptKey); $authService->loadTenantDataIntoSession($userId); if ($remember) { $rememberMeService->rememberUser($userId); } $sessionStore->set('session_started_at', time()); $sessionStore->set('session_last_activity', time()); Router::redirect($intendedUrlService->retrieveAndForget($sessionStore)); return; } } } } } else { $stage = 'resolve_email'; $setLoginWarning(t('We could not continue with these login details')); } } } } } } } $errors = $errorBag->toFlatList();