[SECURITY][EVENT] Remove deprecated uses of Symfony Guard. Add LoginSucess and LoginFailure events

This commit is contained in:
Hugo Sales 2022-03-08 22:32:18 +00:00
parent 4d77f3497d
commit 301421ea15
No known key found for this signature in database
GPG Key ID: 7D0C7EAFC9D835A0
6 changed files with 38 additions and 289 deletions

View File

@ -24,28 +24,26 @@ security:
pattern: ^/oauth pattern: ^/oauth
security: false security: false
main: main:
entry_point: App\Security\Authenticator lazy: true
guard:
authenticators:
- App\Security\Authenticator
provider: local_user provider: local_user
form_login: form_login:
login_path: security_login login_path: security_login
check_path: security_login check_path: security_login
enable_csrf: true
logout: logout:
path: security_logout path: security_logout
# where to redirect after logout # where to redirect after logout
target: root target: root
remember_me: # remember_me:
secret: '%kernel.secret%' # secret: '%kernel.secret%'
secure: true # secure: true
httponly: '%remember_me_httponly%' # httponly: '%remember_me_httponly%'
samesite: '%remember_me_samesite%' # samesite: '%remember_me_samesite%'
token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider' # token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'
# activate different ways to authenticate # custom_authenticator: 'App\Core\Security'
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication # https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html # https://symfony.com/doc/current/security/impersonating_user.html

View File

@ -14,8 +14,6 @@ use App\Core\Log;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Feed; use App\Entity\Feed;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Security\Authenticator;
use App\Security\EmailVerifier;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\DuplicateFoundException;
use App\Util\Exception\EmailException; use App\Util\Exception\EmailException;
@ -40,8 +38,8 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
@ -95,8 +93,8 @@ class Security extends Controller
public function register( public function register(
Request $request, Request $request,
UserPasswordHasherInterface $user_password_hasher, UserPasswordHasherInterface $user_password_hasher,
Authenticator $authenticator, // \App\Core\Security $authenticator,
GuardAuthenticatorHandler $guard, // UserAuthenticatorInterface $user_authenticator,
): array|Response { ): array|Response {
$form = Form::create([ $form = Form::create([
['nickname', TextType::class, [ ['nickname', TextType::class, [
@ -207,12 +205,7 @@ class Security extends Controller
$user->setIsEmailVerified(true); $user->setIsEmailVerified(true);
} }
return $guard->authenticateUserAndHandleSuccess( // return $user_authenticator->authenticateUser($user, $authenticator, $request);
$user,
$request,
$authenticator,
'main',
);
} }
return [ return [

View File

@ -49,7 +49,6 @@ use App\Core\I18n\I18n;
use App\Core\Queue\Queue; use App\Core\Queue\Queue;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Kernel; use App\Kernel;
use App\Security\EmailVerifier;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ConfigurationException; use App\Util\Exception\ConfigurationException;
use App\Util\Formatting; use App\Util\Formatting;
@ -176,7 +175,6 @@ class GNUsocial implements EventSubscriberInterface
Router::setRouter($this->router); Router::setRouter($this->router);
HTTPClient::setClient($this->client); HTTPClient::setClient($this->client);
Formatting::setTwig($this->twig); Formatting::setTwig($this->twig);
EmailVerifier::setHelpers($this->email_verify_helper, $this->mailer_helper);
Cache::setupCache(); Cache::setupCache();
DB::initTableMap(); DB::initTableMap();

View File

@ -34,7 +34,10 @@ namespace App\Core;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use BadMethodCallException; use BadMethodCallException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security as SymfonySecurity; use Symfony\Component\Security\Core\Security as SymfonySecurity;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
/** /**
* Forwards method calls to either Symfony\Component\Security\Core\Security or * Forwards method calls to either Symfony\Component\Security\Core\Security or
@ -45,15 +48,34 @@ use Symfony\Component\Security\Core\Security as SymfonySecurity;
* *
* @method static LocalUser getUser() * @method static LocalUser getUser()
*/ */
abstract class Security class Security implements EventSubscriberInterface //implements AuthenticatorInterface
{ {
private static ?SymfonySecurity $security; private static ?SymfonySecurity $security;
public static function setHelper($sec): void public static function setHelper($sec): void
{ {
self::$security = $sec; self::$security = $sec;
} }
public function loginSucess(LoginSuccessEvent $event): LoginSuccessEvent
{
Event::handle('LoginSuccess', [$event]);
return $event;
}
public function loginFailure(LoginFailureEvent $event): LoginFailureEvent
{
Event::handle('LoginFailure', [$event]);
return $event;
}
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => 'loginSucess',
LoginFailureEvent::class => 'loginFailure',
];
}
public static function __callStatic(string $name, array $args) public static function __callStatic(string $name, array $args)
{ {
if (method_exists(self::$security, $name)) { if (method_exists(self::$security, $name)) {

View File

@ -1,201 +0,0 @@
<?php
declare(strict_types = 1);
// {{{ 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 function App\Core\I18n\_m;
use App\Core\Log;
use App\Core\Router\Router;
use App\Entity\LocalUser;
use App\Util\Common;
use App\Util\Exception\ClientException;
use App\Util\Exception\NoSuchActorException;
use App\Util\Exception\NotFoundException;
use App\Util\Exception\ServerException;
use App\Util\Nickname;
use Stringable;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
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\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* User authenticator
*
* @category Authentication
* @package GNUsocial
*
* @author Hugo Sales <hugo@hsal.es>
* @copyright 2020-2021 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 implements AuthenticatorInterface
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'security_login';
private CsrfTokenManagerInterface $csrfTokenManager;
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
{
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request): bool
{
return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
}
/**
* @return array<string, string>
*/
public function getCredentials(Request $request): array
{
return [
'nickname_or_email' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
}
/**
* Get a user given credentials and a CSRF token
*
* @param array<string, string> $credentials result of self::getCredentials
*
* @throws NoSuchActorException
* @throws ServerException
*
* @return ?LocalUser
*/
public function getUser($credentials, UserProviderInterface $userProvider): ?LocalUser
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = null;
try {
if (Common::isValidEmail($credentials['nickname_or_email'])) {
$user = LocalUser::getByEmail($credentials['nickname_or_email']);
} elseif (Nickname::isValid($credentials['nickname_or_email'])) {
$user = LocalUser::getByNickname($credentials['nickname_or_email']);
}
if (\is_null($user)) {
throw new NoSuchActorException('No such local user.');
}
$credentials['nickname'] = $user->getNickname();
} catch (NoSuchActorException|NotFoundException) {
throw new CustomUserMessageAuthenticationException(
_m('Invalid login credentials.'),
);
}
return $user;
}
/**
* @param array<string, string> $credentials result of self::getCredentials
* @param LocalUser $user
*
* @throws ServerException
*/
public function checkCredentials($credentials, $user): bool
{
if (!$user->checkPassword($credentials['password'])) {
throw new CustomUserMessageAuthenticationException(_m('Invalid login credentials.'));
} else {
return true;
}
}
/**
* After a successful login, redirect user to the path saved in their session or to the root of the website
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse
{
$nickname = $token->getUser();
if ($nickname instanceof Stringable) {
$nickname = (string) $nickname;
} elseif ($nickname instanceof UserInterface) {
$nickname = $nickname->getUserIdentifier();
}
$request->getSession()->set(
Security::LAST_USERNAME,
$nickname,
);
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
} elseif (!\is_null($next = $request->request->get('_next') ?? $request->query->get('next'))) {
try {
if ($pos = mb_strrpos($next, '#')) {
$fragment = mb_substr($next, $pos);
$next = mb_substr($next, 0, $pos);
}
Router::match($next);
return new RedirectResponse(url: $next . ($fragment ?? ''));
} catch (ResourceNotFoundException $e) {
$user = Common::user();
$user_id = !\is_null($user) ? $user->getId() : '(not logged in)';
Log::warning("Suspicious activity: User with ID {$user_id} submitted a form where the `_next` parameter is not a valid local URL ({$next})");
throw new ClientException(_m('Invalid form submission'), $e);
}
} else {
return new RedirectResponse(url: Router::url('root'));
}
}
public function authenticate(Request $request): PassportInterface
{
$nickname = $request->request->get('nickname', '');
$request->getSession()->set(Security::LAST_USERNAME, $nickname);
return new Passport(
new UserBadge($nickname),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
],
);
}
protected function getLoginUrl(): string
{
return Router::url(self::LOGIN_ROUTE);
}
}

View File

@ -1,61 +0,0 @@
<?php
declare(strict_types = 1);
namespace App\Security;
use App\Core\DB\DB;
use App\Entity\LocalUser;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
abstract class EmailVerifier
{
private static $verifyEmailHelper;
private static $mailer;
public static function setHelpers(VerifyEmailHelperInterface $helper, MailerInterface $mailer)
{
self::$verifyEmailHelper = $helper;
self::$mailer = $mailer;
}
/**
* @param LocalUser $user
*/
public static function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = self::$verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->getId(),
$user->getOutgoingEmail(),
['id' => $user->getId()],
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
self::$mailer->send($email);
}
/**
* @param LocalUser $user
*
* @throws VerifyEmailExceptionInterface
*/
public static function handleEmailConfirmation(Request $request, UserInterface $user): void
{
self::$verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getOutgoingEmail());
$user->setIsEmailVerified(true);
DB::persist($user);
DB::flush();
}
}