[UI][SESSION] Add login and logout pages
This commit is contained in:
		| @@ -1,8 +1,8 @@ | |||||||
| doctrine: | doctrine: | ||||||
|     dbal: |     dbal: | ||||||
|         # TODO In case of special URL characters, this needs to be handled differently |  | ||||||
|         url: '%env(resolve:DATABASE_URL)%' |         url: '%env(resolve:DATABASE_URL)%' | ||||||
|         charset: UTF8 |         charset: UTF8 | ||||||
|  |         schema_filter: ~^(?!rememberme_token)~ # Ignore these in migrations | ||||||
|     orm: |     orm: | ||||||
|         auto_generate_proxy_classes: true |         auto_generate_proxy_classes: true | ||||||
|         naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware |         naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware | ||||||
|   | |||||||
| @@ -10,6 +10,20 @@ security: | |||||||
|             anonymous: true |             anonymous: true | ||||||
|             lazy: true |             lazy: true | ||||||
|             provider: users_in_memory |             provider: users_in_memory | ||||||
|  |             guard: | ||||||
|  |                 authenticators: | ||||||
|  |                     - App\Security\Authenticator | ||||||
|  |             logout: | ||||||
|  |                 path: logout | ||||||
|  |                 # where to redirect after logout | ||||||
|  |                 target: main_all | ||||||
|  |  | ||||||
|  |             remember_me: | ||||||
|  |                 secret: '%kernel.secret%' | ||||||
|  |                 secure: true | ||||||
|  |                 httponly: '%remember_me_httponly%' | ||||||
|  |                 samesite: '%remember_me_samesite%' | ||||||
|  |                 token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider' | ||||||
|  |  | ||||||
|             # activate different ways to authenticate |             # activate different ways to authenticate | ||||||
|             # https://symfony.com/doc/current/security.html#firewalls-authentication |             # https://symfony.com/doc/current/security.html#firewalls-authentication | ||||||
| @@ -20,5 +34,5 @@ security: | |||||||
|     # Easy way to control access for large sections of your site |     # Easy way to control access for large sections of your site | ||||||
|     # Note: Only the *first* access control that matches will be used |     # Note: Only the *first* access control that matches will be used | ||||||
|     access_control: |     access_control: | ||||||
|         # - { path: ^/admin, roles: ROLE_ADMIN } |         - { path: ^/admin, roles: ROLE_ADMIN } | ||||||
|         # - { path: ^/profile, roles: ROLE_USER } |         - { path: ^/settings, roles: ROLE_USER } | ||||||
|   | |||||||
| @@ -43,3 +43,5 @@ services: | |||||||
|  |  | ||||||
|     App\Core\Queue\MessageHandler: |     App\Core\Queue\MessageHandler: | ||||||
|       tags: [messenger.message_handler] |       tags: [messenger.message_handler] | ||||||
|  |  | ||||||
|  |     Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider: ~ | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								src/Controller/SecurityController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/Controller/SecurityController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace App\Controller; | ||||||
|  |  | ||||||
|  | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||||
|  | use Symfony\Component\HttpFoundation\Response; | ||||||
|  | use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; | ||||||
|  |  | ||||||
|  | class SecurityController extends AbstractController | ||||||
|  | { | ||||||
|  |     public function login(AuthenticationUtils $authenticationUtils): Response | ||||||
|  |     { | ||||||
|  |         if ($this->getUser()) { | ||||||
|  |             return $this->redirectToRoute('main_all'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // get the login error if there is one | ||||||
|  |         $error = $authenticationUtils->getLastAuthenticationError(); | ||||||
|  |         // last username entered by the user | ||||||
|  |         $lastUsername = $authenticationUtils->getLastUsername(); | ||||||
|  |  | ||||||
|  |         return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function logout() | ||||||
|  |     { | ||||||
|  |         throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								src/Core/UserRoles.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/Core/UserRoles.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | // {{{ License | ||||||
|  | // This file is part of GNU social - https://www.gnu.org/software/social | ||||||
|  | // | ||||||
|  | // GNU social is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // GNU social is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | // }}} | ||||||
|  |  | ||||||
|  | namespace App\Core; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * User role enum | ||||||
|  |  * | ||||||
|  |  * @category  User | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * | ||||||
|  |  * @author    Hugo Sales <hugo@fc.up.pt> | ||||||
|  |  * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org | ||||||
|  |  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||||
|  |  */ | ||||||
|  | abstract class UserRoles | ||||||
|  | { | ||||||
|  |     const ADMIN     = 1; | ||||||
|  |     const MODERATOR = 2; | ||||||
|  |     const USER      = 4; | ||||||
|  |  | ||||||
|  |     public static function bitmapToStrings(int $r): array | ||||||
|  |     { | ||||||
|  |         $roles  = []; | ||||||
|  |         $consts = (new ReflectionClass(__CLASS__))->getConstants(); | ||||||
|  |         while ($r != 0) { | ||||||
|  |             foreach ($consts as $c => $v) { | ||||||
|  |                 if ($r & $v !== 0) { | ||||||
|  |                     $r &= ~$v; | ||||||
|  |                     $roles[] = "ROLE_{$c}"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $roles; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								src/Entity/RememberMeToken.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/Entity/RememberMeToken.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | // {{{ License | ||||||
|  |  | ||||||
|  | // This file is part of GNU social - https://www.gnu.org/software/social | ||||||
|  | // | ||||||
|  | // GNU social is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // GNU social is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | // }}} | ||||||
|  |  | ||||||
|  | namespace App\Entity; | ||||||
|  |  | ||||||
|  | use DateTimeInterface; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Entity for the remember_me token | ||||||
|  |  * | ||||||
|  |  * @category  DB | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * | ||||||
|  |  * @author    Hugo Sales <hugo@fc.up.pt> | ||||||
|  |  * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org | ||||||
|  |  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||||
|  |  */ | ||||||
|  | class RememberMeToken | ||||||
|  | { | ||||||
|  |     // {{{ Autocode | ||||||
|  |  | ||||||
|  |     private string $series; | ||||||
|  |     private string $value; | ||||||
|  |     private \DateTimeInterface $lastUsed; | ||||||
|  |     private string $class; | ||||||
|  |     private string $username; | ||||||
|  |  | ||||||
|  |     public function setSeries(string $series): self | ||||||
|  |     { | ||||||
|  |         $this->series = $series; | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |     public function getSeries(): string | ||||||
|  |     { | ||||||
|  |         return $this->series; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setValue(string $value): self | ||||||
|  |     { | ||||||
|  |         $this->value = $value; | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |     public function getValue(): string | ||||||
|  |     { | ||||||
|  |         return $this->value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setLastUsed(DateTimeInterface $lastUsed): self | ||||||
|  |     { | ||||||
|  |         $this->lastUsed = $lastUsed; | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |     public function getLastUsed(): DateTimeInterface | ||||||
|  |     { | ||||||
|  |         return $this->lastUsed; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setClass(string $class): self | ||||||
|  |     { | ||||||
|  |         $this->class = $class; | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |     public function getClass(): string | ||||||
|  |     { | ||||||
|  |         return $this->class; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function setUsername(string $username): self | ||||||
|  |     { | ||||||
|  |         $this->username = $username; | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  |     public function getUsername(): string | ||||||
|  |     { | ||||||
|  |         return $this->username; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // }}} Autocode | ||||||
|  |  | ||||||
|  |     public static function schemaDef(): array | ||||||
|  |     { | ||||||
|  |         $def = [ | ||||||
|  |             'name'   => 'rememberme_token', | ||||||
|  |             'fields' => [ | ||||||
|  |                 'series'   => ['type' => 'char', 'length' => 88, 'not null' => true], | ||||||
|  |                 'value'    => ['type' => 'char', 'length' => 88, 'not null' => true], | ||||||
|  |                 'lastUsed' => ['type' => 'datetime',  'not null' => true, 'default' => 'CURRENT_TIMESTAMP'], | ||||||
|  |                 'class'    => ['type' => 'varchar', 'length' => 100, 'not null' => true], | ||||||
|  |                 'username' => ['type' => 'varchar', 'length' => 64, 'not null' => true], | ||||||
|  |             ], | ||||||
|  |             'primary key' => ['series'], | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         return $def; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -40,7 +40,10 @@ abstract class Main | |||||||
|     public static function load(RouteLoader $r): void |     public static function load(RouteLoader $r): void | ||||||
|     { |     { | ||||||
|         $r->connect('main_all', '/main/all', C\NetworkPublic::class); |         $r->connect('main_all', '/main/all', C\NetworkPublic::class); | ||||||
|         $r->connect('config_admin', '/config/admin', C\AdminConfigController::class); |         $r->connect('admin_config', '/admin/config', C\AdminConfigController::class); | ||||||
|  |  | ||||||
|  |         $r->connect('login', '/login', [C\SecurityController::class, 'login']); | ||||||
|  |         $r->connect('logout', '/logout', [C\SecurityController::class, 'logout']); | ||||||
|  |  | ||||||
|         // FAQ static pages |         // FAQ static pages | ||||||
|         foreach (['faq', 'contact', 'tags', 'groups', 'openid'] as $s) { |         foreach (['faq', 'contact', 'tags', 'groups', 'openid'] as $s) { | ||||||
|   | |||||||
							
								
								
									
										148
									
								
								src/Security/Authenticator.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/Security/Authenticator.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | // {{{ License | ||||||
|  | // This file is part of GNU social - https://www.gnu.org/software/social | ||||||
|  | // | ||||||
|  | // GNU social is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // GNU social is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | // }}} | ||||||
|  |  | ||||||
|  | namespace App\Security; | ||||||
|  |  | ||||||
|  | use App\Core\DB\DB; | ||||||
|  | use function App\Core\I18n\_m; | ||||||
|  | use App\Core\Log; | ||||||
|  | use App\Entity\User; | ||||||
|  | use App\Util\Nickname; | ||||||
|  | use Doctrine\ORM\EntityManagerInterface; | ||||||
|  | use Symfony\Component\HttpFoundation\RedirectResponse; | ||||||
|  | use Symfony\Component\HttpFoundation\Request; | ||||||
|  | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||||
|  | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||||
|  | use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | ||||||
|  | use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; | ||||||
|  | use Symfony\Component\Security\Core\Security; | ||||||
|  | use Symfony\Component\Security\Core\User\UserInterface; | ||||||
|  | use Symfony\Component\Security\Core\User\UserProviderInterface; | ||||||
|  | use Symfony\Component\Security\Csrf\CsrfToken; | ||||||
|  | use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; | ||||||
|  | use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; | ||||||
|  | use Symfony\Component\Security\Http\Util\TargetPathTrait; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * User authenticator | ||||||
|  |  * | ||||||
|  |  * @category  Authentication | ||||||
|  |  * @package   GNUsocial | ||||||
|  |  * | ||||||
|  |  * @author    Hugo Sales <hugo@fc.up.pt> | ||||||
|  |  * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org | ||||||
|  |  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||||
|  |  */ | ||||||
|  | class Authenticator extends AbstractFormLoginAuthenticator | ||||||
|  | { | ||||||
|  |     use TargetPathTrait; | ||||||
|  |  | ||||||
|  |     public const LOGIN_ROUTE = 'login'; | ||||||
|  |  | ||||||
|  |     private $entityManager; | ||||||
|  |     private $urlGenerator; | ||||||
|  |     private $csrfTokenManager; | ||||||
|  |  | ||||||
|  |     public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager) | ||||||
|  |     { | ||||||
|  |         $this->entityManager    = $entityManager; | ||||||
|  |         $this->urlGenerator     = $urlGenerator; | ||||||
|  |         $this->csrfTokenManager = $csrfTokenManager; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function supports(Request $request) | ||||||
|  |     { | ||||||
|  |         return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getCredentials(Request $request) | ||||||
|  |     { | ||||||
|  |         $credentials = [ | ||||||
|  |             'nickname'   => $request->request->get('nickname'), | ||||||
|  |             'password'   => $request->request->get('password'), | ||||||
|  |             'csrf_token' => $request->request->get('_csrf_token'), | ||||||
|  |         ]; | ||||||
|  |         $request->getSession()->set( | ||||||
|  |             Security::LAST_USERNAME, | ||||||
|  |             $credentials['nickname'] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return $credentials; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function getUser($credentials, UserProviderInterface $userProvider) | ||||||
|  |     { | ||||||
|  |         $token = new CsrfToken('authenticate', $credentials['csrf_token']); | ||||||
|  |         if (!$this->csrfTokenManager->isTokenValid($token)) { | ||||||
|  |             throw new InvalidCsrfTokenException(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $nick = Nickname::normalize($credentials['nickname']); | ||||||
|  |         $user = DB::findOneBy('local_user', ['or' => ['nickname' => $nick, 'outgoing_email' => $nick]]); | ||||||
|  |  | ||||||
|  |         if (!$user) { | ||||||
|  |             throw new CustomUserMessageAuthenticationException( | ||||||
|  |                 _m('Either \'{nickname}\' doesn\'t match any registered nickname or email, or the supplied password is incorrect.', ['{nickname}' => $credentials['nickname']])); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return $user; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function checkCredentials($credentials, UserInterface $user) | ||||||
|  |     { | ||||||
|  |         $password = $user->getPassword(); | ||||||
|  |         Log::error(print_r($user, true)); | ||||||
|  |         // crypt understands what the salt part of $user->password is | ||||||
|  |         if ($password === crypt($credentials['password'], $user->password)) { | ||||||
|  |             $this->changePassword($user->nickname, null, $password); | ||||||
|  |             return $user; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // If we check StatusNet hash, for backwards compatibility and migration | ||||||
|  |         if ($this->statusnet && $user->password === md5($password . $user->id)) { | ||||||
|  |             // and update password hash entry to crypt() compatible | ||||||
|  |             if ($this->overwrite) { | ||||||
|  |                 $this->changePassword($user->nickname, null, $password); | ||||||
|  |             } | ||||||
|  |             return $user; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Timing safe password verification on supported PHP versions | ||||||
|  |         if (password_verify($password, $user->password)) { | ||||||
|  |             return $user; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | ||||||
|  |     { | ||||||
|  |         if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { | ||||||
|  |             return new RedirectResponse($targetPath); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); | ||||||
|  |         throw new \Exception('TODO: provide a valid redirect inside ' . __FILE__); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected function getLoginUrl() | ||||||
|  |     { | ||||||
|  |         return $this->urlGenerator->generate(self::LOGIN_ROUTE); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								templates/security/login.html.twig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								templates/security/login.html.twig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | {% extends 'base.html.twig' %} | ||||||
|  |  | ||||||
|  | {% block title %}Log in!{% endblock %} | ||||||
|  |  | ||||||
|  | {% block body %} | ||||||
|  |   <form method="post"> | ||||||
|  |     {% if error %} | ||||||
|  |       <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     {% if app.user %} | ||||||
|  |       <div class="mb-3"> | ||||||
|  |         You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a> | ||||||
|  |       </div> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> | ||||||
|  |     <label for="inputNickname">Nickname</label> | ||||||
|  |     <input type="text" value="{{ last_username }}" name="nickname" id="inputNickname" class="form-control" required autofocus> | ||||||
|  |     <label for="inputPassword">Password</label> | ||||||
|  |     <input type="password" name="password" id="inputPassword" class="form-control" required> | ||||||
|  |     <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"> | ||||||
|  |  | ||||||
|  |     <div class="checkbox mb-3"> | ||||||
|  |       <label> | ||||||
|  |         <input type="checkbox" name="_remember_me"> Remember me | ||||||
|  |       </label> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <button class="btn btn-lg btn-primary" type="submit"> | ||||||
|  |       Sign in | ||||||
|  |     </button> | ||||||
|  |   </form> | ||||||
|  | {% endblock %} | ||||||
		Reference in New Issue
	
	Block a user