[UI][SESSION] Add login and logout pages

This commit is contained in:
Hugo Sales 2020-07-22 01:58:25 +00:00 committed by Hugo Sales
parent fb53700be2
commit 3313897671
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
9 changed files with 400 additions and 4 deletions

View File

@ -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

View File

@ -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 }

View File

@ -43,3 +43,5 @@ services:
App\Core\Queue\MessageHandler:
tags: [messenger.message_handler]
Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider: ~

View 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
View 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;
}
}

View 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;
}
}

View File

@ -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) {

View 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);
}
}

View 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 %}