first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<p>Váš ověřovací kód je: {ldelim}2FA_KOD{rdelim} </p>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Controller;
|
||||
|
||||
use KupShop\KupShopBundle\Routing\TranslatedRoute;
|
||||
use KupShop\TwoFactorBundle\View\TwoFactorView;
|
||||
use KupShop\UserBundle\View\LoginView;
|
||||
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Email\Generator\CodeGeneratorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class TwoFactorController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @TranslatedRoute("/#2fa_check#/")
|
||||
*/
|
||||
public function twoFactorAction(TwoFactorView $view)
|
||||
{
|
||||
return $view->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/2fa_resend")
|
||||
*/
|
||||
public function resendTwoFactorAction(CodeGeneratorInterface $codeGenerator, Security $security)
|
||||
{
|
||||
$user = $security->getUser();
|
||||
if ($user instanceof TwoFactorInterface) {
|
||||
$codeGenerator->reSend($user);
|
||||
}
|
||||
|
||||
return new RedirectResponse(path('2fa_login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/login/check")
|
||||
*/
|
||||
public function checkAction(Request $request, LoginView $view)
|
||||
{
|
||||
$view->setRequest($request);
|
||||
|
||||
return $view->getResponse();
|
||||
}
|
||||
}
|
||||
37
bundles/KupShop/TwoFactorBundle/Email/TwoFactorCodeEmail.php
Normal file
37
bundles/KupShop/TwoFactorBundle/Email/TwoFactorCodeEmail.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Email;
|
||||
|
||||
use KupShop\KupShopBundle\Email\BaseEmail;
|
||||
|
||||
class TwoFactorCodeEmail extends BaseEmail
|
||||
{
|
||||
protected static $name = 'Ověřovací kód';
|
||||
protected $subject = 'Ověřovací kód';
|
||||
protected static $type = 'TWO_FACTOR';
|
||||
protected static $priority = 0;
|
||||
protected $template = 'email/two_factor.tpl';
|
||||
|
||||
public static function getPlaceholders()
|
||||
{
|
||||
$placeholders = [
|
||||
'2FA_KOD' => [
|
||||
'text' => 'Ověřovací kód',
|
||||
],
|
||||
];
|
||||
|
||||
return [self::$type => $placeholders] + (parent::getPlaceholders() ?? []);
|
||||
}
|
||||
|
||||
public function replacePlaceholdersItem($placeholder)
|
||||
{
|
||||
// Samotný replace je přímo v maileru, tohle tu je jen kvůli testovacímu mailu
|
||||
if ($placeholder == '2FA_KOD') {
|
||||
return '123456';
|
||||
}
|
||||
|
||||
return parent::replacePlaceholdersItem($placeholder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\EventSubscriber;
|
||||
|
||||
use KupShop\KupShopBundle\Context\UserContext;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class ExceptionSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
#[Required]
|
||||
public LoggerInterface $logger;
|
||||
|
||||
#[Required]
|
||||
public UserContext $userContext;
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
KernelEvents::EXCEPTION => [
|
||||
['handleRedirect', 200],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function handleRedirect(ExceptionEvent $event)
|
||||
{
|
||||
$exception = $event->getThrowable();
|
||||
|
||||
$redirectSet = false;
|
||||
if ($exception instanceof AccessDeniedHttpException || $exception instanceof AccessDeniedException) {
|
||||
if ($exception->getMessage() == 'User is not in a two-factor authentication process.') {
|
||||
$redirectSet = true;
|
||||
$event->setResponse(new RedirectResponse(path('home')));
|
||||
}
|
||||
|
||||
$email = 'none';
|
||||
if ($user = $this->userContext->getActive()) {
|
||||
$email = $user->email;
|
||||
}
|
||||
|
||||
$this->logger->notice("[TwoFactorBundle] Handle exception, user: {$email}", [
|
||||
'exception' => $exception->getMessage(),
|
||||
'exception_class' => get_class($exception),
|
||||
'redirect_set' => $redirectSet,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\EventSubscriber;
|
||||
|
||||
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
|
||||
class LoginSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
LoginSuccessEvent::class => [
|
||||
['redirectToTwoFactorForm', 200],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* By default after login, the user is redirected to homepage/next url which redirects him to 2FA form - this does not work with CDN/Cache.
|
||||
* This forces user in 2FA process to be redirected to 2FA form directly.
|
||||
*/
|
||||
public function redirectToTwoFactorForm(LoginSuccessEvent $event)
|
||||
{
|
||||
if ($event->getAuthenticatedToken() instanceof TwoFactorTokenInterface) {
|
||||
$event->setResponse(new RedirectResponse(path('2fa_login', referenceType: Router::ABSOLUTE_URL)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
imports:
|
||||
- { resource: 'security.yml' }
|
||||
- { resource: 'scheb_2fa.yml' }
|
||||
@@ -0,0 +1,8 @@
|
||||
2fa_login:
|
||||
path: /2fa_login
|
||||
defaults:
|
||||
_controller: "KupShop\\TwoFactorBundle\\Controller\\TwoFactorController::twoFactorAction"
|
||||
|
||||
content:
|
||||
resource: "@TwoFactorBundle/Controller/"
|
||||
type: annotation
|
||||
@@ -0,0 +1,9 @@
|
||||
scheb_two_factor:
|
||||
persister: KupShop\TwoFactorBundle\Util\UserPersister
|
||||
email:
|
||||
digits: 6
|
||||
enabled: true
|
||||
sender_email: no-reply@example.com
|
||||
# sender_name: John Doe # Optional
|
||||
# form_renderer: KupShop\UserBundle\Render\EmailFormRenderer
|
||||
mailer: KupShop\TwoFactorBundle\Util\TwoFactorMailer
|
||||
@@ -0,0 +1,18 @@
|
||||
security:
|
||||
firewalls:
|
||||
admin:
|
||||
pattern: ^/(admin|_z)/
|
||||
main:
|
||||
two_factor:
|
||||
provider: kupshop_userbundle
|
||||
auth_form_path: 2fa_login
|
||||
check_path: 2fa_login
|
||||
prepare_on_login: true
|
||||
prepare_on_access_denied: true
|
||||
always_use_default_target_path: false
|
||||
enable_csrf: false
|
||||
failure_handler: KupShop\TwoFactorBundle\Util\AuthenticationFailureHandler
|
||||
form_login:
|
||||
use_referer: true
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
KupShop\TwoFactorBundle\:
|
||||
resource: ../../{Controller,View,Util,Security,Email,EventSubscriber}
|
||||
exclude: ../../Security/TwoFactorUser.php
|
||||
|
||||
KupShop\UserBundle\Security\UserProvider:
|
||||
class: KupShop\TwoFactorBundle\Security\TwoFactorUserProvider
|
||||
|
||||
KupShop\TwoFactorBundle\Util\TwoFactorMailer:
|
||||
calls:
|
||||
- setSmsHandler: ['@?KupShop\TelfaBundle\Utils\SMSHandler']
|
||||
autowire: true
|
||||
51
bundles/KupShop/TwoFactorBundle/Security/TwoFactorUser.php
Normal file
51
bundles/KupShop/TwoFactorBundle/Security/TwoFactorUser.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Security;
|
||||
|
||||
use KupShop\UserBundle\Security\User;
|
||||
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
|
||||
|
||||
class TwoFactorUser extends User implements TwoFactorInterface
|
||||
{
|
||||
private ?string $authCode;
|
||||
|
||||
public function isEmailAuthEnabled(): bool
|
||||
{
|
||||
$settings = \Settings::getDefault();
|
||||
$ignoredGroups = $settings['two_factor']['ignored_groups'] ?? [];
|
||||
|
||||
foreach ($ignoredGroups as $ignoredGroup) {
|
||||
if ($this->getKupshopUser()->hasGroupId($ignoredGroup)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return findModule(\Modules::TWO_FACTOR) && ($settings['two_factor']['enabled'] ?? false);
|
||||
}
|
||||
|
||||
public function getEmailAuthRecipient(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getEmailAuthCode(): string
|
||||
{
|
||||
$code = $this->authCode ?? null;
|
||||
|
||||
if (!$code) {
|
||||
$code = $this->getKupshopUser()->getCustomData()['email_code'];
|
||||
if (!$code) {
|
||||
throw new \LogicException('The email authentication code was not set');
|
||||
}
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
public function setEmailAuthCode(string $authCode): void
|
||||
{
|
||||
$this->authCode = $authCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Security;
|
||||
|
||||
use KupShop\UserBundle\Security\User;
|
||||
use KupShop\UserBundle\Security\UserProvider;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
|
||||
class TwoFactorUserProvider extends UserProvider
|
||||
{
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
if (!$email = $this->getEmailByUsername($username)) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
$user = \User::createFromLogin($email);
|
||||
if (!isset($user)) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
return new TwoFactorUser($user->id, $user->email, $user->passw, ['ROLE_USER'], $user);
|
||||
}
|
||||
|
||||
public function supportsClass($class): bool
|
||||
{
|
||||
return $class === TwoFactorUser::class || $class === User::class;
|
||||
}
|
||||
}
|
||||
9
bundles/KupShop/TwoFactorBundle/TwoFactorBundle.php
Normal file
9
bundles/KupShop/TwoFactorBundle/TwoFactorBundle.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\TwoFactorBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class TwoFactorBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Util;
|
||||
|
||||
use Scheb\TwoFactorBundle\Security\Authentication\Exception\InvalidTwoFactorCodeException;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\TwoFactorFirewallContext;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected HttpUtils $httpUtils,
|
||||
protected TwoFactorFirewallContext $firewallContext)
|
||||
{
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
$request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
|
||||
$firewallConfig = $this->firewallContext->getFirewallConfig('main');
|
||||
|
||||
if ($exception instanceof InvalidTwoFactorCodeException) {
|
||||
addUserMessage(translate('invalidCode', 'two_factor'), 'error');
|
||||
}
|
||||
|
||||
return $this->httpUtils->createRedirectResponse($request, $firewallConfig->getAuthFormPath());
|
||||
}
|
||||
}
|
||||
48
bundles/KupShop/TwoFactorBundle/Util/TwoFactorMailer.php
Normal file
48
bundles/KupShop/TwoFactorBundle/Util/TwoFactorMailer.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Util;
|
||||
|
||||
use KupShop\KupShopBundle\Util\Mail\SMSSendingInterface;
|
||||
use KupShop\TwoFactorBundle\Email\TwoFactorCodeEmail;
|
||||
use KupShop\TwoFactorBundle\Security\TwoFactorUser;
|
||||
use Scheb\TwoFactorBundle\Mailer\AuthCodeMailerInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class TwoFactorMailer implements AuthCodeMailerInterface
|
||||
{
|
||||
#[Required]
|
||||
public TwoFactorCodeEmail $email;
|
||||
|
||||
protected SMSSendingInterface $SMSHandler;
|
||||
|
||||
public function sendAuthCode(TwoFactorInterface $user): void
|
||||
{
|
||||
if (!$user instanceof TwoFactorUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authCode = $user->getEmailAuthCode();
|
||||
|
||||
// Na review chci poslat mail, telfa tam není aktivní
|
||||
if ((findModule(\Modules::TELFA) || findModule(\Modules::DAKTELA)) && !isDevelopment()) {
|
||||
$phone = $user->getKupshopUser()['phone'];
|
||||
if (empty($text = $this->email->getSMSText(['2FA_KOD' => $authCode]))) {
|
||||
$text = $authCode;
|
||||
}
|
||||
$this->SMSHandler->sendSMS($phone, $text);
|
||||
} else {
|
||||
$email = $this->email->getEmail(['2FA_KOD' => $authCode]);
|
||||
$email['to'] = $user->getEmailAuthRecipient();
|
||||
$this->email->sendEmail($email);
|
||||
}
|
||||
}
|
||||
|
||||
#[Required]
|
||||
public function setSmsHandler(SMSSendingInterface $SMSHandler)
|
||||
{
|
||||
$this->SMSHandler = $SMSHandler;
|
||||
}
|
||||
}
|
||||
19
bundles/KupShop/TwoFactorBundle/Util/UserPersister.php
Normal file
19
bundles/KupShop/TwoFactorBundle/Util/UserPersister.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\Util;
|
||||
|
||||
use KupShop\UserBundle\Security\User;
|
||||
use Scheb\TwoFactorBundle\Model\PersisterInterface;
|
||||
|
||||
class UserPersister implements PersisterInterface
|
||||
{
|
||||
/**
|
||||
* @param User $user
|
||||
*/
|
||||
public function persist($user): void
|
||||
{
|
||||
$user->getKupshopUser()->setCustomData('email_code', $user->getEmailAuthCode());
|
||||
}
|
||||
}
|
||||
42
bundles/KupShop/TwoFactorBundle/View/TwoFactorView.php
Normal file
42
bundles/KupShop/TwoFactorBundle/View/TwoFactorView.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\TwoFactorBundle\View;
|
||||
|
||||
use KupShop\KupShopBundle\Views\View;
|
||||
use KupShop\TwoFactorBundle\Security\TwoFactorUser;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class TwoFactorView extends View
|
||||
{
|
||||
#[Required]
|
||||
public Security $security;
|
||||
protected $template = 'two_factor.tpl';
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return translate('title', 'two_factor');
|
||||
}
|
||||
|
||||
public function getBreadcrumbs()
|
||||
{
|
||||
return getReturnNavigation(-1, 'NO_TYPE', [translate('returnNav', 'two_factor')]);
|
||||
}
|
||||
|
||||
public function getBodyVariables()
|
||||
{
|
||||
$vars = parent::getBodyVariables();
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof TwoFactorUser) {
|
||||
$phone = $user->getKupshopUser()->phone;
|
||||
if ($phone) {
|
||||
$vars['phone'] = substr($phone, -3);
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user