first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1 @@
<p>Váš ověřovací kód je: {ldelim}2FA_KOD{rdelim} </p>

View File

@@ -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();
}
}

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

View File

@@ -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,
]);
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
imports:
- { resource: 'security.yml' }
- { resource: 'scheb_2fa.yml' }

View File

@@ -0,0 +1,8 @@
2fa_login:
path: /2fa_login
defaults:
_controller: "KupShop\\TwoFactorBundle\\Controller\\TwoFactorController::twoFactorAction"
content:
resource: "@TwoFactorBundle/Controller/"
type: annotation

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\TwoFactorBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class TwoFactorBundle extends Bundle
{
}

View File

@@ -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());
}
}

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

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

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