diff --git a/nginx.conf b/nginx.conf index 85b176b01b..b73deea78b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -129,7 +129,7 @@ server { rewrite admin/api/(.*) /admin/api/index.php last; # Administration pages - rewrite admin/(attachments|backup|category|comments|configuration|elasticsearch|export|faq|faqs|forms|glossary|group|import|instance|instances|logout|media-browser|news|password|questions|session-keep-alive|statistics|sticky-faqs|stopwords|system|tags|update|user) /admin/front.php last; + rewrite admin/(attachments|authenticate|backup|category|comments|configuration|elasticsearch|export|faq|faqs|forms|glossary|group|import|instance|instances|logout|login|media-browser|news|password|questions|session-keep-alive|statistics|sticky-faqs|stopwords|system|tags|update|user) /admin/front.php last; # REST API v3.0 and v3.1 rewrite ^api/v3\.[01]/(.*) /api/index.php last; diff --git a/phpmyfaq/.htaccess b/phpmyfaq/.htaccess index 4e4d1c7e4f..b721fd3314 100644 --- a/phpmyfaq/.htaccess +++ b/phpmyfaq/.htaccess @@ -143,7 +143,7 @@ Header set Access-Control-Allow-Headers "Content-Type, Authorization" # Administration API RewriteRule ^admin/api/(.*) admin/api/index.php [L,QSA] # Administration pages - RewriteRule ^admin/(attachments|backup|category|comments|configuration|elasticsearch|export|faq|faqs|forms|glossary|group|import|instance|instances|logout|media-browser|news|password|questions|session-keep-alive|statistics|sticky-faqs|stopwords|system|tags|update|user) admin/front.php [L,QSA] + RewriteRule ^admin/(attachments|authenticate|backup|category|comments|configuration|elasticsearch|export|faq|faqs|forms|glossary|group|import|instance|instances|login|logout|media-browser|news|password|questions|session-keep-alive|statistics|sticky-faqs|stopwords|system|tags|update|user) admin/front.php [L,QSA] #RewriteRule ^admin/(.*) admin/front.php [L,QSA] # Private APIs RewriteRule ^api/(autocomplete|bookmark/delete|bookmark/create|user/data/update|user/password/update|user/request-removal|user/remove-twofactor|contact|voting|register|captcha|share|comment/create|faq/create|question/create|webauthn/prepare|webauthn/register|webauthn/prepare-login|webauthn/login) api/index.php [L,QSA] diff --git a/phpmyfaq/admin/index.php b/phpmyfaq/admin/index.php index 3666cfb8c4..c349ea4ec0 100755 --- a/phpmyfaq/admin/index.php +++ b/phpmyfaq/admin/index.php @@ -35,6 +35,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -264,7 +265,8 @@ // User is NOT authenticated } else { //$error = Translation::get('msgSessionExpired'); - require 'login.php'; + $redirect = new RedirectResponse('./login'); + $redirect->send(); } require 'footer.php'; diff --git a/phpmyfaq/admin/login.php b/phpmyfaq/admin/login.php deleted file mode 100644 index f94e0daef0..0000000000 --- a/phpmyfaq/admin/login.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @author Alexander M. Turek - * @copyright 2013-2024 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2013-02-05 - */ - -use phpMyFAQ\Configuration; -use phpMyFAQ\Strings; -use phpMyFAQ\Template\TwigWrapper; -use phpMyFAQ\Translation; -use Symfony\Component\HttpFoundation\Request; - -$faqConfig = Configuration::getConfigurationInstance(); - -if (isset($error) && 0 < strlen((string) $error)) { - $errorMessage = $error; -} else { - $errorMessage = ''; -} - -$request = Request::createFromGlobals(); - -$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates'); -$template = $twig->loadTemplate('@admin/login.twig'); - -$templateVars = [ - 'isSecure' => $request->isSecure() || !$faqConfig->get('security.useSslForLogins'), - 'isError' => isset($error) && 0 < strlen((string) $error), - 'errorMessage' => $errorMessage, - 'loginMessage' => Translation::get('ad_auth_insert'), - 'isLogout' => $request->query->get('action') === 'logout', - 'logoutMessage' => Translation::get('ad_logout'), - 'loginUrl' => $faqConfig->getDefaultUrl() . 'admin/index.php', - 'redirectAction' => Strings::htmlentities($request->query->get('action') ?? '') , - 'msgUsername' => Translation::get('ad_auth_user'), - 'msgPassword' => Translation::get('ad_auth_passwd'), - 'msgRememberMe' => Translation::get('rememberMe'), - 'msgLostPassword' => Translation::get('lostPassword'), - 'msgLoginUser' => Translation::get('msgLoginUser'), - 'hasRegistrationEnabled' => $faqConfig->get('security.enableRegistration'), - 'msgRegistration' => Translation::get('msgRegistration'), - 'hasSignInWithMicrosoftActive' => $faqConfig->isSignInWithMicrosoftActive(), - 'msgSignInWithMicrosoft' => Translation::get('msgSignInWithMicrosoft'), - 'secureUrl' => sprintf('https://%s%s', $request->getHost(), $request->getRequestUri()), - 'msgNotSecure' => Translation::get('msgSecureSwitch'), - 'isWebAuthnEnabled' => $faqConfig->get('security.enableWebAuthnSupport'), -]; - -echo $template->render($templateVars); diff --git a/phpmyfaq/assets/templates/admin/login.twig b/phpmyfaq/assets/templates/admin/login.twig index 0b9829e309..50f7cff708 100644 --- a/phpmyfaq/assets/templates/admin/login.twig +++ b/phpmyfaq/assets/templates/admin/login.twig @@ -1,93 +1,97 @@ -{% if isSecure %} -
-
-
-
-
-
-
-
-

phpMyFAQ Login

+{% extends '@admin/index.twig' %} - {% if isError %} - - {% else %} -

- {{ loginMessage }} -

- {% endif %} +{% block content %} - {% if isLogout %} - -
-
- -
- - + {% if isSecure %} +
+
+
+
+
+
+
+
+

phpMyFAQ Login

+ + {% if isError %} + -
-
- - + {% else %} +

+ {{ loginMessage }} +

+ {% endif %} + + {% if isLogout %} + +
+ + +
+ +
- +
+
+ + +
+ -
-
- - -
-
- {{ msgLostPassword }} - -
- -
- +
+ + +
+
+ {{ msgLostPassword }} + +
+ +
+
-
-
+
+
- -{% else %} - -{% endif %} + {% else %} + + {% endif %} +{% endblock %} diff --git a/phpmyfaq/src/admin-routes.php b/phpmyfaq/src/admin-routes.php index 9675d4cfd7..b993e44489 100644 --- a/phpmyfaq/src/admin-routes.php +++ b/phpmyfaq/src/admin-routes.php @@ -56,6 +56,16 @@ 'controller' => [AttachmentsController::class, 'index'], 'methods' => 'GET' ], + 'admin.auth.authenticate' => [ + 'path' => '/authenticate', + 'controller' => [AuthenticationController::class, 'authenticate'], + 'methods' => 'POST' + ], + 'admin.auth.login' => [ + 'path' => '/login', + 'controller' => [AuthenticationController::class, 'login'], + 'methods' => 'GET' + ], 'admin.auth.logout' => [ 'path' => '/logout', 'controller' => [AuthenticationController::class, 'logout'], diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/AbstractAdministrationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/AbstractAdministrationController.php index f4abcd3180..dbab2cf617 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/AbstractAdministrationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/AbstractAdministrationController.php @@ -301,7 +301,7 @@ protected function getHeader(Request $request): array break; } - if ($this->configuration->get('main.enableGravatarSupport')) { + if ($this->currentUser->isLoggedIn() && $this->configuration->get('main.enableGravatarSupport')) { $avatar = new Gravatar(); $gravatarImage = $avatar->getImage( $this->currentUser->getUserData('email'), diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/AuthenticationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/AuthenticationController.php index 67038f2151..efea54a8c2 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/AuthenticationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/AuthenticationController.php @@ -4,8 +4,12 @@ namespace phpMyFAQ\Controller\Administration; +use phpMyFAQ\Core\Exception; use phpMyFAQ\Filter; use phpMyFAQ\Session\Token; +use phpMyFAQ\Translation; +use phpMyFAQ\User\UserAuthentication; +use phpMyFAQ\User\UserException; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -13,6 +17,93 @@ class AuthenticationController extends AbstractAdministrationController { + #[Route('/authenticate', name: 'admin.auth.authenticate', methods: ['POST'])] + public function authenticate(Request $request): Response + { + if ($this->currentUser->isLoggedIn()) { + return new RedirectResponse('./'); + } + + $logging = $this->container->get('phpmyfaq.admin.admin-log'); + + $username = Filter::filterVar($request->get('faqusername'), FILTER_SANITIZE_SPECIAL_CHARS); + $password = Filter::filterVar( + $request->get('faqpassword'), + FILTER_SANITIZE_SPECIAL_CHARS, + FILTER_FLAG_NO_ENCODE_QUOTES + ); + $rememberMe = Filter::filterVar($request->get('faqrememberme'), FILTER_VALIDATE_BOOLEAN); + + // Set username via SSO + if ($this->configuration->get('security.ssoSupport') && $request->server->get('REMOTE_USER') !== null) { + $username = trim((string) $request->server->get('REMOTE_USER')); + $password = ''; + } + + // Login via local DB or LDAP or SSO + if ($username !== '' && ($password !== '' || $this->configuration->get('security.ssoSupport'))) { + $userAuth = new UserAuthentication($this->configuration, $this->currentUser); + $userAuth->setRememberMe($rememberMe ?? false); + try { + $this->currentUser = $userAuth->authenticate($username, $password); + if ($userAuth->hasTwoFactorAuthentication()) { + return new RedirectResponse('./2fa'); + } + } catch (Exception $e) { + $logging->log( + $this->currentUser, + 'Login-error\nLogin: ' . $username . '\nErrors: ' . implode(', ', $this->configuration->errors) + ); + //$error = $e->getMessage(); + return new RedirectResponse('./login'); + } + } + + return new RedirectResponse('./'); + } + + /** + * @throws UserException + * @throws Exception + * @throws \Exception + */ + #[Route('/login', name: 'admin.auth.logout', methods: ['GET'])] + public function login(Request $request): Response + { + // Redirect to authenticate if SSO is enabled and the user is already authenticated + if ($this->configuration->get('security.ssoSupport') && $request->server->get('REMOTE_USER') !== null) { + return new RedirectResponse('./authenticate'); + } + + return $this->render( + '@admin/login.twig', + [ + ... $this->getHeader($request), + ... $this->getFooter(), + 'isSecure' => $request->isSecure() || !$this->configuration->get('security.useSslForLogins'), + 'isError' => isset($error) && 0 < strlen((string) $error), + 'errorMessage' => 'to be implemented', + 'loginMessage' => Translation::get('ad_auth_insert'), + 'isLogout' => $request->query->get('action') === 'logout', + 'logoutMessage' => Translation::get('ad_logout'), + 'loginUrl' => $this->configuration->getDefaultUrl() . 'admin/authenticate', + 'redirectAction' => $request->query->get('action') ?? '' , + 'msgUsername' => Translation::get('ad_auth_user'), + 'msgPassword' => Translation::get('ad_auth_passwd'), + 'msgRememberMe' => Translation::get('rememberMe'), + 'msgLostPassword' => Translation::get('lostPassword'), + 'msgLoginUser' => Translation::get('msgLoginUser'), + 'hasRegistrationEnabled' => $this->configuration->get('security.enableRegistration'), + 'msgRegistration' => Translation::get('msgRegistration'), + 'hasSignInWithMicrosoftActive' => $this->configuration->isSignInWithMicrosoftActive(), + 'msgSignInWithMicrosoft' => Translation::get('msgSignInWithMicrosoft'), + 'secureUrl' => sprintf('https://%s%s', $request->getHost(), $request->getRequestUri()), + 'msgNotSecure' => Translation::get('msgSecureSwitch'), + 'isWebAuthnEnabled' => $this->configuration->get('security.enableWebAuthnSupport'), + ] + ); + } + /** * @throws \Exception */