[LOGIN] Implement password checking and related systems

This commit is contained in:
Hugo Sales 2020-07-22 11:45:03 +00:00 committed by Hugo Sales
parent f3ccdf8017
commit 0eba267a73
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
5 changed files with 71 additions and 31 deletions

View File

@ -1,7 +1,10 @@
security: security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers: providers:
users_in_memory: { memory: null } users:
entity:
class: 'App\Entity\LocalUser'
property: 'nickname'
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
@ -9,7 +12,7 @@ security:
main: main:
anonymous: true anonymous: true
lazy: true lazy: true
provider: users_in_memory provider: users
guard: guard:
authenticators: authenticators:
- App\Security\Authenticator - App\Security\Authenticator

View File

@ -51,14 +51,18 @@ use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class GNUsocial implements EventSubscriberInterface class GNUsocial implements EventSubscriberInterface
{ {
use TargetPathTrait;
protected LoggerInterface $logger; protected LoggerInterface $logger;
protected TranslatorInterface $translator; protected TranslatorInterface $translator;
protected EntityManagerInterface $entity_manager; protected EntityManagerInterface $entity_manager;
@ -66,6 +70,7 @@ class GNUsocial implements EventSubscriberInterface
protected FormFactoryInterface $form_factory; protected FormFactoryInterface $form_factory;
protected MessageBusInterface $message_bus; protected MessageBusInterface $message_bus;
protected EventDispatcherInterface $event_dispatcher; protected EventDispatcherInterface $event_dispatcher;
protected SessionInterface $session;
/** /**
* Symfony dependency injection gives us access to these services * Symfony dependency injection gives us access to these services
@ -76,7 +81,8 @@ class GNUsocial implements EventSubscriberInterface
RouterInterface $router, RouterInterface $router,
FormFactoryInterface $ff, FormFactoryInterface $ff,
MessageBusInterface $mb, MessageBusInterface $mb,
EventDispatcherInterface $ed) EventDispatcherInterface $ed,
SessionInterface $s)
{ {
$this->logger = $logger; $this->logger = $logger;
$this->translator = $trans; $this->translator = $trans;
@ -85,6 +91,7 @@ class GNUsocial implements EventSubscriberInterface
$this->form_factory = $ff; $this->form_factory = $ff;
$this->message_bus = $mb; $this->message_bus = $mb;
$this->event_dispatcher = $ed; $this->event_dispatcher = $ed;
$this->session = $s;
$this->register(); $this->register();
} }
@ -122,6 +129,12 @@ class GNUsocial implements EventSubscriberInterface
public function onKernelRequest(RequestEvent $event, public function onKernelRequest(RequestEvent $event,
string $event_name): RequestEvent string $event_name): RequestEvent
{ {
$request = $event->getRequest();
if (!(!$event->isMasterRequest() || $request->isXmlHttpRequest()
|| 'login' === $request->attributes->get('_route'))) {
$this->saveTargetPath($this->session, 'main', $request->getUri());
}
$this->register(); $this->register();
return $event; return $event;
} }

View File

@ -19,7 +19,9 @@
namespace App\Entity; namespace App\Entity;
use App\Core\DB\DB;
use App\Core\UserRoles; use App\Core\UserRoles;
use App\Util\Common;
use DateTimeInterface; use DateTimeInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@ -307,4 +309,47 @@ class LocalUser implements UserInterface
public function eraseCredentials() public function eraseCredentials()
{ {
} }
public function checkPassword(string $new_password): bool
{
// Timing safe password verification on supported PHP versions
if (password_verify($new_password, $this->getPassword())) {
return true;
}
// Old format
// crypt understands what the salt part of $this->getPassword() is
if ($this->getPassword() === crypt($new_password, $this->getPassword())) {
$this->changePassword($new_password, true);
return true;
}
return false;
}
public function changePassword(string $new_password, bool $override = false): void
{
if ($override || $this->checkPassword($new_password)) {
$this->setPassword($this->hashPassword($new_password));
DB::flush();
}
}
public function hashPassword(string $password)
{
switch (Common::config('security', 'algorithm')) {
case 'bcrypt':
$algorithm = PASSWORD_BCRYPT;
break;
case 'argon2i':
$algorithm = PASSWORD_ARGON2I;
break;
case 'argon2id':
$algorithm = PASSWORD_ARGON2ID;
break;
}
$options = Common::config('security', 'options');
return password_hash($password, $algorithm, $options);
}
} }

View File

@ -21,7 +21,6 @@ namespace App\Security;
use App\Core\DB\DB; use App\Core\DB\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\User; use App\Entity\User;
use App\Util\Nickname; use App\Util\Nickname;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -95,10 +94,9 @@ class Authenticator extends AbstractFormLoginAuthenticator
$nick = Nickname::normalize($credentials['nickname']); $nick = Nickname::normalize($credentials['nickname']);
$user = DB::findOneBy('local_user', ['or' => ['nickname' => $nick, 'outgoing_email' => $nick]]); $user = DB::findOneBy('local_user', ['or' => ['nickname' => $nick, 'outgoing_email' => $nick]]);
if (!$user) { if (!$user) {
throw new CustomUserMessageAuthenticationException( throw new CustomUserMessageAuthenticationException(
_m('Either \'{nickname}\' doesn\'t match any registered nickname or email, or the supplied password is incorrect.', ['{nickname}' => $credentials['nickname']])); _m('\'{nickname}\' doesn\'t match any registered nickname or email.', ['{nickname}' => $credentials['nickname']]));
} }
return $user; return $user;
@ -106,29 +104,11 @@ class Authenticator extends AbstractFormLoginAuthenticator
public function checkCredentials($credentials, UserInterface $user) public function checkCredentials($credentials, UserInterface $user)
{ {
$password = $user->getPassword(); if (!$user->checkPassword($credentials['password'])) {
Log::error(print_r($user, true)); throw new CustomUserMessageAuthenticationException(_m('Invalid login credentials.'));
// crypt understands what the salt part of $user->password is } else {
if ($password === crypt($credentials['password'], $user->password)) { return true;
$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) public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
@ -137,8 +117,7 @@ class Authenticator extends AbstractFormLoginAuthenticator
return new RedirectResponse($targetPath); return new RedirectResponse($targetPath);
} }
// For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); return new RedirectResponse($this->urlGenerator->generate('main_all'));
throw new \Exception('TODO: provide a valid redirect inside ' . __FILE__);
} }
protected function getLoginUrl() protected function getLoginUrl()

View File

@ -42,7 +42,7 @@ abstract class Common
{ {
$c = DB::find('config', ['section' => $section, 'setting' => $setting]); $c = DB::find('config', ['section' => $section, 'setting' => $setting]);
if ($c === null) { if ($c === null) {
throw new Exception("The field section = {$section} and setting = {$setting} doesn't exist"); throw new \Exception("The field section = {$section} and setting = {$setting} doesn't exist");
} }
return unserialize($c->getValue()); return unserialize($c->getValue());