[UI][SESSION] Add login and logout pages
This commit is contained in:
		| @@ -1,8 +1,8 @@ | ||||
| doctrine: | ||||
|     dbal: | ||||
|         # TODO In case of special URL characters, this needs to be handled differently | ||||
|         url: '%env(resolve:DATABASE_URL)%' | ||||
|         charset: UTF8 | ||||
|         schema_filter: ~^(?!rememberme_token)~ # Ignore these in migrations | ||||
|     orm: | ||||
|         auto_generate_proxy_classes: true | ||||
|         naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware | ||||
|   | ||||
| @@ -10,6 +10,20 @@ security: | ||||
|             anonymous: true | ||||
|             lazy: true | ||||
|             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 | ||||
|             # 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 | ||||
|     # Note: Only the *first* access control that matches will be used | ||||
|     access_control: | ||||
|         # - { path: ^/admin, roles: ROLE_ADMIN } | ||||
|         # - { path: ^/profile, roles: ROLE_USER } | ||||
|         - { path: ^/admin, roles: ROLE_ADMIN } | ||||
|         - { path: ^/settings, roles: ROLE_USER } | ||||
|   | ||||
| @@ -43,3 +43,5 @@ services: | ||||
|  | ||||
|     App\Core\Queue\MessageHandler: | ||||
|       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 | ||||
|     { | ||||
|         $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 | ||||
|         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