forked from GNUsocial/gnu-social
[CORE][SECURITY] Move to the new authentication format, for Symfony 5.3
Keep using (deprecated) Guard
This commit is contained in:
parent
b4ce77320e
commit
3a5e52ee0d
@ -1,6 +1,10 @@
|
|||||||
security:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
enable_authenticator_manager: true
|
||||||
|
password_hashers:
|
||||||
|
App\Entity\LocalUser:
|
||||||
|
algorithm: auto
|
||||||
providers:
|
providers:
|
||||||
|
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
||||||
local_user:
|
local_user:
|
||||||
chain:
|
chain:
|
||||||
providers: [local_user_by_nickname, local_user_by_email]
|
providers: [local_user_by_nickname, local_user_by_email]
|
||||||
@ -11,18 +15,21 @@ security:
|
|||||||
local_user_by_email:
|
local_user_by_email:
|
||||||
entity:
|
entity:
|
||||||
class: 'App\Entity\LocalUser'
|
class: 'App\Entity\LocalUser'
|
||||||
property: 'email'
|
property: 'outgoing_email'
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
main:
|
||||||
anonymous: true
|
entry_point: App\Security\Authenticator
|
||||||
lazy: true
|
|
||||||
provider: local_user
|
|
||||||
guard:
|
guard:
|
||||||
authenticators:
|
authenticators:
|
||||||
- App\Security\Authenticator
|
- App\Security\Authenticator
|
||||||
|
provider: local_user
|
||||||
|
form_login:
|
||||||
|
login_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
|
||||||
|
@ -52,3 +52,4 @@ services:
|
|||||||
resource: '../components/*'
|
resource: '../components/*'
|
||||||
exclude: '../components/*/{scripts,classes,lib,actions,locale,doc}'
|
exclude: '../components/*/{scripts,classes,lib,actions,locale,doc}'
|
||||||
tags: ['controller.service_arguments']
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class AdminPanel extends Controller
|
|||||||
*/
|
*/
|
||||||
public function site(Request $request)
|
public function site(Request $request)
|
||||||
{
|
{
|
||||||
// TODO CHECK PERMISSION
|
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||||
$defaults = Common::getConfigDefaults();
|
$defaults = Common::getConfigDefaults();
|
||||||
$options = [];
|
$options = [];
|
||||||
foreach ($defaults as $key => $inner) {
|
foreach ($defaults as $key => $inner) {
|
||||||
|
@ -8,12 +8,9 @@ use App\Core\Controller;
|
|||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\Form;
|
use App\Core\Form;
|
||||||
use function App\Core\I18n\_m;
|
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Core\VisibilityScope;
|
|
||||||
use App\Entity\Actor;
|
use App\Entity\Actor;
|
||||||
use App\Entity\LocalUser;
|
use App\Entity\LocalUser;
|
||||||
use App\Entity\Note;
|
|
||||||
use App\Entity\Subscription;
|
use App\Entity\Subscription;
|
||||||
use App\Security\Authenticator;
|
use App\Security\Authenticator;
|
||||||
use App\Security\EmailVerifier;
|
use App\Security\EmailVerifier;
|
||||||
@ -37,10 +34,12 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
|||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
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\Security\Guard\GuardAuthenticatorHandler;
|
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
|
||||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
|
|
||||||
class Security extends Controller
|
class Security extends Controller
|
||||||
{
|
{
|
||||||
@ -49,6 +48,7 @@ class Security extends Controller
|
|||||||
*/
|
*/
|
||||||
public function login(AuthenticationUtils $authenticationUtils)
|
public function login(AuthenticationUtils $authenticationUtils)
|
||||||
{
|
{
|
||||||
|
// Skip if already logged in
|
||||||
if ($this->getUser()) {
|
if ($this->getUser()) {
|
||||||
return $this->redirectToRoute('main_all');
|
return $this->redirectToRoute('main_all');
|
||||||
}
|
}
|
||||||
@ -90,9 +90,10 @@ class Security extends Controller
|
|||||||
*/
|
*/
|
||||||
public function register(
|
public function register(
|
||||||
Request $request,
|
Request $request,
|
||||||
GuardAuthenticatorHandler $guard_handler,
|
UserPasswordHasherInterface $user_password_hasher,
|
||||||
Authenticator $authenticator,
|
Authenticator $authenticator,
|
||||||
): array|Response|null {
|
GuardAuthenticatorHandler $guard,
|
||||||
|
): array|Response {
|
||||||
$form = Form::create([
|
$form = Form::create([
|
||||||
['nickname', TextType::class, [
|
['nickname', TextType::class, [
|
||||||
'label' => _m('Nickname'),
|
'label' => _m('Nickname'),
|
||||||
@ -100,10 +101,9 @@ class Security extends Controller
|
|||||||
'constraints' => [
|
'constraints' => [
|
||||||
new NotBlank(['message' => _m('Please enter a nickname')]),
|
new NotBlank(['message' => _m('Please enter a nickname')]),
|
||||||
new Length([
|
new Length([
|
||||||
'min' => 1,
|
|
||||||
'minMessage' => _m(['Your nickname must be at least # characters long'], ['count' => 1]),
|
|
||||||
'max' => Nickname::MAX_LEN,
|
'max' => Nickname::MAX_LEN,
|
||||||
'maxMessage' => _m(['Your nickname must be at most # characters long'], ['count' => Nickname::MAX_LEN]), ]),
|
'maxMessage' => _m(['Your nickname must be at most # characters long'], ['count' => Nickname::MAX_LEN]),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
'block_name' => 'nickname',
|
'block_name' => 'nickname',
|
||||||
'label_attr' => ['class' => 'section-form-label'],
|
'label_attr' => ['class' => 'section-form-label'],
|
||||||
@ -116,8 +116,9 @@ class Security extends Controller
|
|||||||
'block_name' => 'email',
|
'block_name' => 'email',
|
||||||
'label_attr' => ['class' => 'section-form-label'],
|
'label_attr' => ['class' => 'section-form-label'],
|
||||||
'invalid_message' => _m('Email not valid. Please provide a valid email.'),
|
'invalid_message' => _m('Email not valid. Please provide a valid email.'),
|
||||||
|
'attr' => ['autocomplete' => 'email'],
|
||||||
]],
|
]],
|
||||||
FormFields::repeated_password(),
|
FormFields::repeated_password(['attr' => ['autocomplete' => 'new-password']]),
|
||||||
['register', SubmitType::class, ['label' => _m('Register')]],
|
['register', SubmitType::class, ['label' => _m('Register')]],
|
||||||
], form_options: ['block_prefix' => 'register']);
|
], form_options: ['block_prefix' => 'register']);
|
||||||
|
|
||||||
@ -128,15 +129,11 @@ class Security extends Controller
|
|||||||
$data['password'] = $form->get('password')->getData();
|
$data['password'] = $form->get('password')->getData();
|
||||||
|
|
||||||
// Already used is checked below
|
// Already used is checked below
|
||||||
$sanitized_nickname = null;
|
$nickname = Nickname::normalize($data['nickname'], check_already_used: false, which: Nickname::CHECK_LOCAL_USER, check_is_allowed: false);
|
||||||
if (Event::handle('SanitizeNickname', [$data['nickname'], &$sanitized_nickname]) != Event::stop) {
|
|
||||||
$sanitized_nickname = $data['nickname'];
|
|
||||||
// $sanitized_nickname = Nickname::normalize($data['nickname'], check_already_used: false, which: Nickname::CHECK_LOCAL_USER, check_is_allowed: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$found_user = DB::findOneBy('local_user', ['or' => ['nickname' => $sanitized_nickname, 'outgoing_email' => $data['email']]]);
|
$found_user = DB::findOneBy('local_user', ['or' => ['nickname' => $nickname, 'outgoing_email' => $data['email']]]);
|
||||||
if ($found_user->getNickname() === $sanitized_nickname) {
|
if ($found_user->getNickname() === $nickname) {
|
||||||
throw new NicknameTakenException($found_user->getActor());
|
throw new NicknameTakenException($found_user->getActor());
|
||||||
} elseif ($found_user->getOutgoingEmail() === $data['email']) {
|
} elseif ($found_user->getOutgoingEmail() === $data['email']) {
|
||||||
throw new EmailTakenException($found_user->getActor());
|
throw new EmailTakenException($found_user->getActor());
|
||||||
@ -148,13 +145,13 @@ class Security extends Controller
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// This already checks if the nickname is being used
|
// This already checks if the nickname is being used
|
||||||
$actor = Actor::create(['nickname' => $sanitized_nickname, 'is_local' => true]);
|
$actor = Actor::create(['nickname' => $nickname, 'is_local' => true]);
|
||||||
$user = LocalUser::create([
|
$user = LocalUser::create([
|
||||||
'nickname' => $sanitized_nickname,
|
'nickname' => $nickname,
|
||||||
'outgoing_email' => $data['email'],
|
'outgoing_email' => $data['email'],
|
||||||
'incoming_email' => $data['email'],
|
'incoming_email' => $data['email'],
|
||||||
'password' => LocalUser::hashPassword($data['password']),
|
|
||||||
]);
|
]);
|
||||||
|
$user->setPassword($user_password_hasher->hashPassword($user, $data['password']));
|
||||||
DB::persistWithSameId(
|
DB::persistWithSameId(
|
||||||
$actor,
|
$actor,
|
||||||
$user,
|
$user,
|
||||||
@ -169,7 +166,7 @@ class Security extends Controller
|
|||||||
} catch (UniqueConstraintViolationException $e) {
|
} catch (UniqueConstraintViolationException $e) {
|
||||||
// _something_ was duplicated, but since we already check if nickname is in use, we can't tell what went wrong
|
// _something_ was duplicated, but since we already check if nickname is in use, we can't tell what went wrong
|
||||||
$m = 'An error occurred while trying to register';
|
$m = 'An error occurred while trying to register';
|
||||||
Log::critical($m . " with nickname: '{$sanitized_nickname}' and email '{$data['email']}'");
|
Log::critical($m . " with nickname: '{$nickname}' and email '{$data['email']}'");
|
||||||
throw new ServerException(_m($m), previous: $e);
|
throw new ServerException(_m($m), previous: $e);
|
||||||
}
|
}
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
@ -177,24 +174,24 @@ class Security extends Controller
|
|||||||
// generate a signed url and email it to the user
|
// generate a signed url and email it to the user
|
||||||
if ($_ENV['APP_ENV'] !== 'dev' && Common::config('site', 'use_email')) {
|
if ($_ENV['APP_ENV'] !== 'dev' && Common::config('site', 'use_email')) {
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
EmailVerifier::sendEmailConfirmation($user);
|
// TODO: Implement send confirmation email
|
||||||
|
// (new EmailVerifier())->sendEmailConfirmation($user);
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
} else {
|
} else {
|
||||||
$user->setIsEmailVerified(true);
|
$user->setIsEmailVerified(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $guard_handler->authenticateUserAndHandleSuccess(
|
return $guard->authenticateUserAndHandleSuccess(
|
||||||
$user,
|
$user,
|
||||||
$request,
|
$request,
|
||||||
$authenticator,
|
$authenticator,
|
||||||
'main', // firewall name in security.yaml
|
'main',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'_template' => 'security/register.html.twig',
|
'_template' => 'security/register.html.twig',
|
||||||
'registration_form' => $form->createView(),
|
'registration_form' => $form->createView(),
|
||||||
'notes_fn' => fn () => Note::getAllNotes(VisibilityScope::$instance_scope),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -174,7 +173,6 @@ class GNUsocial implements EventSubscriberInterface
|
|||||||
HTTPClient::setClient($this->client);
|
HTTPClient::setClient($this->client);
|
||||||
Formatting::setTwig($this->twig);
|
Formatting::setTwig($this->twig);
|
||||||
Cache::setupCache();
|
Cache::setupCache();
|
||||||
EmailVerifier::setEmailHelpers($this->mailer_helper, $this->email_verify_helper, $this->reset_password_helper);
|
|
||||||
|
|
||||||
DB::initTableMap();
|
DB::initTableMap();
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ use App\Util\Common;
|
|||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,7 +48,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||||
*/
|
*/
|
||||||
class LocalUser extends Entity implements UserInterface
|
class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
{
|
{
|
||||||
// {{{ Autocode
|
// {{{ Autocode
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
@ -301,7 +302,7 @@ class LocalUser extends Entity implements UserInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function hashPassword(string $password)
|
public static function hashPassword(string $password): string
|
||||||
{
|
{
|
||||||
$algorithm = self::algoNameToConstant(Common::config('security', 'algorithm'));
|
$algorithm = self::algoNameToConstant(Common::config('security', 'algorithm'));
|
||||||
$options = Common::config('security', 'options');
|
$options = Common::config('security', 'options');
|
||||||
@ -329,6 +330,29 @@ class LocalUser extends Entity implements UserInterface
|
|||||||
throw new Exception('Unsupported or unsafe hashing algorithm requested');
|
throw new Exception('Unsupported or unsafe hashing algorithm requested');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the username used to authenticate the user.
|
||||||
|
* Part of the Symfony UserInterface
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @deprecated since Symfony 5.3, use getUserIdentifier() instead
|
||||||
|
*/
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
return $this->getUserIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the identifier for this user (e.g. its nickname)
|
||||||
|
* Part of the Symfony UserInterface
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUserIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->getNickname();
|
||||||
|
}
|
||||||
// }}} Authentication
|
// }}} Authentication
|
||||||
|
|
||||||
public function getActor()
|
public function getActor()
|
||||||
@ -344,14 +368,6 @@ class LocalUser extends Entity implements UserInterface
|
|||||||
return UserRoles::toArray($this->getActor()->getRoles());
|
return UserRoles::toArray($this->getActor()->getRoles());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the username used to authenticate the user. Part of the Symfony UserInterface
|
|
||||||
*/
|
|
||||||
public function getUsername()
|
|
||||||
{
|
|
||||||
return $this->nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getByNickname(string $nickname): ?self
|
public static function getByNickname(string $nickname): ?self
|
||||||
{
|
{
|
||||||
$key = str_replace('_', '-', $nickname);
|
$key = str_replace('_', '-', $nickname);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types = 1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// {{{ License
|
// {{{ License
|
||||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||||
@ -21,13 +21,12 @@ declare(strict_types = 1);
|
|||||||
|
|
||||||
namespace App\Security;
|
namespace App\Security;
|
||||||
|
|
||||||
use function App\Core\I18n\_m;
|
|
||||||
use App\Core\Router\Router;
|
use App\Core\Router\Router;
|
||||||
use App\Entity\LocalUser;
|
use App\Entity\LocalUser;
|
||||||
use App\Entity\User;
|
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\Exception\NoSuchActorException;
|
use App\Util\Exception\NoSuchActorException;
|
||||||
use App\Util\Exception\NotFoundException;
|
use App\Util\Exception\NotFoundException;
|
||||||
|
use App\Util\Exception\ServerException;
|
||||||
use App\Util\Nickname;
|
use App\Util\Nickname;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
@ -41,7 +40,9 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|||||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||||
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
||||||
|
use Symfony\Component\Security\Guard\AuthenticatorInterface;
|
||||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User authenticator
|
* User authenticator
|
||||||
@ -53,13 +54,13 @@ use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
|||||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||||
*/
|
*/
|
||||||
class Authenticator extends AbstractFormLoginAuthenticator
|
class Authenticator extends AbstractFormLoginAuthenticator implements AuthenticatorInterface
|
||||||
{
|
{
|
||||||
use TargetPathTrait;
|
use TargetPathTrait;
|
||||||
|
|
||||||
public const LOGIN_ROUTE = 'security_login';
|
public const LOGIN_ROUTE = 'security_login';
|
||||||
|
|
||||||
private $csrfTokenManager;
|
private CsrfTokenManagerInterface $csrfTokenManager;
|
||||||
|
|
||||||
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
|
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
|
||||||
{
|
{
|
||||||
@ -67,9 +68,10 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param Request $request
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function supports(Request $request)
|
public function supports(Request $request): bool
|
||||||
{
|
{
|
||||||
return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
|
return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
|
||||||
}
|
}
|
||||||
@ -77,12 +79,12 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
public function getCredentials(Request $request)
|
public function getCredentials(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'nickname_or_email' => $request->request->get('nickname_or_email'),
|
'nickname_or_email' => $request->request->get('nickname_or_email'),
|
||||||
'password' => $request->request->get('password'),
|
'password' => $request->request->get('password'),
|
||||||
'csrf_token' => $request->request->get('_csrf_token'),
|
'csrf_token' => $request->request->get('_csrf_token'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,10 +92,12 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
* Get a user given credentials and a CSRF token
|
* Get a user given credentials and a CSRF token
|
||||||
*
|
*
|
||||||
* @param array<string, string> $credentials result of self::getCredentials
|
* @param array<string, string> $credentials result of self::getCredentials
|
||||||
*
|
* @param UserProviderInterface $userProvider
|
||||||
* @return ?LocalUser
|
* @return ?LocalUser
|
||||||
|
* @throws NoSuchActorException
|
||||||
|
* @throws ServerException
|
||||||
*/
|
*/
|
||||||
public function getUser($credentials, UserProviderInterface $userProvider)
|
public function getUser($credentials, UserProviderInterface $userProvider): ?LocalUser
|
||||||
{
|
{
|
||||||
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
||||||
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
||||||
@ -106,11 +110,11 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
} elseif (Nickname::isValid($credentials['nickname_or_email'])) {
|
} elseif (Nickname::isValid($credentials['nickname_or_email'])) {
|
||||||
$user = LocalUser::getByNickname($credentials['nickname_or_email']);
|
$user = LocalUser::getByNickname($credentials['nickname_or_email']);
|
||||||
}
|
}
|
||||||
if ($user === null) {
|
if (is_null($user)) {
|
||||||
throw new NoSuchActorException('No such local user.');
|
throw new NoSuchActorException('No such local user.');
|
||||||
}
|
}
|
||||||
$credentials['nickname'] = $user->getNickname();
|
$credentials['nickname'] = $user->getNickname();
|
||||||
} catch (NotFoundException) {
|
} catch (NoSuchActorException|NotFoundException) {
|
||||||
throw new CustomUserMessageAuthenticationException(
|
throw new CustomUserMessageAuthenticationException(
|
||||||
_m('Invalid login credentials.'),
|
_m('Invalid login credentials.'),
|
||||||
);
|
);
|
||||||
@ -120,9 +124,11 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, string> $credentials result of self::getCredentials
|
* @param array<string, string> $credentials result of self::getCredentials
|
||||||
* @param LocalUser $user
|
* @param LocalUser $user
|
||||||
|
* @return bool
|
||||||
|
* @throws ServerException
|
||||||
*/
|
*/
|
||||||
public function checkCredentials($credentials, $user)
|
public function checkCredentials($credentials, $user): bool
|
||||||
{
|
{
|
||||||
if (!$user->checkPassword($credentials['password'])) {
|
if (!$user->checkPassword($credentials['password'])) {
|
||||||
throw new CustomUserMessageAuthenticationException(_m('Invalid login credentials.'));
|
throw new CustomUserMessageAuthenticationException(_m('Invalid login credentials.'));
|
||||||
@ -134,13 +140,13 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
/**
|
/**
|
||||||
* After a successful login, redirect user to the path saved in their session or to the root of the website
|
* 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)
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): RedirectResponse
|
||||||
{
|
{
|
||||||
$nickname = $token->getUser();
|
$nickname = $token->getUser();
|
||||||
if ($nickname instanceof Stringable) {
|
if ($nickname instanceof Stringable) {
|
||||||
$nickname = (string) $nickname;
|
$nickname = (string)$nickname;
|
||||||
} elseif ($nickname instanceof UserInterface) {
|
} elseif ($nickname instanceof UserInterface) {
|
||||||
$nickname = $nickname->getUsername();
|
$nickname = $nickname->getUserIdentifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->getSession()->set(
|
$request->getSession()->set(
|
||||||
@ -155,7 +161,7 @@ class Authenticator extends AbstractFormLoginAuthenticator
|
|||||||
return new RedirectResponse(Router::url('main_all'));
|
return new RedirectResponse(Router::url('main_all'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getLoginUrl()
|
protected function getLoginUrl(): string
|
||||||
{
|
{
|
||||||
return Router::url(self::LOGIN_ROUTE);
|
return Router::url(self::LOGIN_ROUTE);
|
||||||
}
|
}
|
||||||
|
@ -4,109 +4,56 @@ declare(strict_types = 1);
|
|||||||
|
|
||||||
namespace App\Security;
|
namespace App\Security;
|
||||||
|
|
||||||
use App\Controller\ResetPassword;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use App\Core\DB\DB;
|
|
||||||
use function App\Core\I18n\_m;
|
|
||||||
use App\Entity\LocalUser;
|
|
||||||
use App\Util\Common;
|
|
||||||
use App\Util\Exception\NotFoundException;
|
|
||||||
use App\Util\Exception\RedirectException;
|
|
||||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Mime\Address;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
|
|
||||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
|
||||||
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
|
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
|
||||||
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
||||||
|
|
||||||
abstract class EmailVerifier
|
class EmailVerifier
|
||||||
{
|
{
|
||||||
private static ?MailerInterface $mailer_helper;
|
private $verifyEmailHelper;
|
||||||
private static ?VerifyEmailHelperInterface $verify_email_helper;
|
private $mailer;
|
||||||
private static ?ResetPasswordHelperInterface $reset_password_helper;
|
private $entityManager;
|
||||||
|
|
||||||
public static function setEmailHelpers(MailerInterface $mailer, VerifyEmailHelperInterface $email_helper, ResetPasswordHelperInterface $reset_helper)
|
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
|
||||||
{
|
{
|
||||||
self::$mailer_helper = $mailer;
|
$this->verifyEmailHelper = $helper;
|
||||||
self::$verify_email_helper = $email_helper;
|
$this->mailer = $mailer;
|
||||||
self::$reset_password_helper = $reset_helper;
|
$this->entityManager = $manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function validateTokenAndFetchUser(string $token)
|
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
|
||||||
{
|
{
|
||||||
return self::$reset_password_helper->validateTokenAndFetchUser($token);
|
$signatureComponents = $this->verifyEmailHelper->generateSignature(
|
||||||
}
|
$verifyEmailRouteName,
|
||||||
|
|
||||||
public static function removeResetRequest(string $token): void
|
|
||||||
{
|
|
||||||
self::$reset_password_helper->removeResetRequest($token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function sendEmailConfirmation(LocalUser $user): void
|
|
||||||
{
|
|
||||||
$email = (new TemplatedEmail())
|
|
||||||
->from(new Address(Common::config('site', 'email'), Common::config('site', 'nickname')))
|
|
||||||
->to($user->getOutgoingEmail())
|
|
||||||
->subject(_m('Please Confirm your Email'))
|
|
||||||
->htmlTemplate('security/confirmation_email.html.twig');
|
|
||||||
|
|
||||||
$signatureComponents = self::$verify_email_helper->generateSignature(
|
|
||||||
'verify_email',
|
|
||||||
$user->getId(),
|
$user->getId(),
|
||||||
$user->getOutgoingEmail(),
|
$user->getOutgoingEmail(),
|
||||||
|
['id' => $user->getId()],
|
||||||
);
|
);
|
||||||
|
|
||||||
$context = $email->getContext();
|
$context = $email->getContext();
|
||||||
$context['signedUrl'] = $signatureComponents->getSignedUrl();
|
$context['signedUrl'] = $signatureComponents->getSignedUrl();
|
||||||
$context['expiresAt'] = $signatureComponents->getExpiresAt();
|
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
|
||||||
|
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
|
||||||
|
|
||||||
$email->context($context);
|
$email->context($context);
|
||||||
|
|
||||||
self::send($email);
|
$this->mailer->send($email);
|
||||||
}
|
|
||||||
|
|
||||||
public static function send($email): void
|
|
||||||
{
|
|
||||||
self::$mailer_helper->send($email);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws VerifyEmailExceptionInterface
|
* @throws VerifyEmailExceptionInterface
|
||||||
*/
|
*/
|
||||||
public function handleEmailConfirmation(Request $request, LocalUser $user): void
|
public function handleEmailConfirmation(Request $request, UserInterface $user): void
|
||||||
{
|
{
|
||||||
self::$verify_email_helper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getOutgoingEmail());
|
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getOutgoingEmail());
|
||||||
$user->setIsEmailVerified(true);
|
|
||||||
DB::persist($user);
|
|
||||||
DB::flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function processSendingPasswordResetEmail(string $emailFormData, ResetPassword $controller)
|
$user->setIsVerified(true);
|
||||||
{
|
|
||||||
try {
|
|
||||||
$user = DB::findOneBy('local_user', ['outgoing_email' => $emailFormData]);
|
|
||||||
$reset_token = self::$reset_password_helper->generateResetToken($user);
|
|
||||||
// Found a user
|
|
||||||
} catch (NotFoundException|ResetPasswordExceptionInterface) {
|
|
||||||
// Not found, do not reveal whether a user account was found or not.
|
|
||||||
throw new RedirectException('check_email');
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = (new TemplatedEmail())
|
$this->entityManager->persist($user);
|
||||||
->from(new Address('foo@email.com', 'FOO NAME'))
|
$this->entityManager->flush();
|
||||||
->to($user->getOutgoingEmail())
|
|
||||||
->subject('Your password reset request')
|
|
||||||
->htmlTemplate('reset_password/email.html.twig')
|
|
||||||
->context([
|
|
||||||
'resetToken' => $reset_token,
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::send($email);
|
|
||||||
|
|
||||||
// Store the token object in session for retrieval in check-email route.
|
|
||||||
$controller->setInSession($reset_token);
|
|
||||||
|
|
||||||
throw new RedirectException('check_email');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ declare(strict_types = 1);
|
|||||||
namespace App\Util;
|
namespace App\Util;
|
||||||
|
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
|
use App\Core\Log;
|
||||||
use App\Util\Exception\DuplicateFoundException;
|
use App\Util\Exception\DuplicateFoundException;
|
||||||
use App\Util\Exception\NicknameEmptyException;
|
use App\Util\Exception\NicknameEmptyException;
|
||||||
use App\Util\Exception\NicknameException;
|
use App\Util\Exception\NicknameException;
|
||||||
@ -184,9 +185,11 @@ class Nickname
|
|||||||
*/
|
*/
|
||||||
public static function normalize(string $nickname, bool $check_already_used = false, int $which = self::CHECK_LOCAL_USER, bool $check_is_allowed = true): string
|
public static function normalize(string $nickname, bool $check_already_used = false, int $which = self::CHECK_LOCAL_USER, bool $check_is_allowed = true): string
|
||||||
{
|
{
|
||||||
|
// Nicknames are lower case and without trailing spaces, it's not offensive to sanitize that to the user
|
||||||
$nickname = trim($nickname);
|
$nickname = trim($nickname);
|
||||||
$nickname = mb_strtolower($nickname);
|
$nickname = mb_strtolower($nickname);
|
||||||
// We could do UTF-8 normalization (å to a, etc.) with something like Normalizer::normalize($nickname, Normalizer::FORM_C)
|
// Anything else would likely be very confusing
|
||||||
|
// We could, e.g., do UTF-8 normalization (å to a, etc.) with something like Normalizer::normalize($nickname, Normalizer::FORM_C)
|
||||||
// We won't as it could confuse tremendously the user, he must know what is valid and should fix his own input
|
// We won't as it could confuse tremendously the user, he must know what is valid and should fix his own input
|
||||||
|
|
||||||
if (!self::validate(nickname: $nickname, check_already_used: $check_already_used, which: $which, check_is_allowed: $check_is_allowed) || !self::isCanonical($nickname)) {
|
if (!self::validate(nickname: $nickname, check_already_used: $check_already_used, which: $which, check_is_allowed: $check_is_allowed) || !self::isCanonical($nickname)) {
|
||||||
|
@ -39,8 +39,9 @@
|
|||||||
|
|
||||||
{# TODO: Login can be done with email, so the element id's should reflect that #}
|
{# TODO: Login can be done with email, so the element id's should reflect that #}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="section-form-label" for="inputNickname">{{ "Nickname or Email" | trans }}</label>
|
<label class="section-form-label" for="inputNicknameOrEmail">{{ "Nickname or Email" | trans }}</label>
|
||||||
<input type="text" value="{{ last_login_id }}" name="nickname_or_email" id="inputNickname" class="form-control" required autofocus>
|
<input type="text" value="{{ last_login_id }}" name="nickname_or_email" id="inputNicknameOrEmail"
|
||||||
|
class="form-control" required autofocus>
|
||||||
<p class="help-text">{{ "Your nickname or email address." | trans }}</p>
|
<p class="help-text">{{ "Your nickname or email address." | trans }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -50,14 +51,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="checkbox mb-3">
|
<span class="checkbox mb-3">
|
||||||
<input type="checkbox" name="_remember_me">
|
<label for="inputRememberMe">{{ "Remember me" | trans }}</label>
|
||||||
<label>{{ "Remember me" | trans }}</label>
|
<input type="checkbox" name="_remember_me" id="inputRememberMe">
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-lg btn-primary" type="submit">Sign in</button>
|
<button class="btn btn-lg btn-primary" type="submit">Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
Loading…
Reference in New Issue
Block a user