<?php
declare(strict_types=1);
namespace Iwv\IwvTwoFactorAuthentication\Subscriber;
use League\OAuth2\Server\Exception\OAuthServerException;
use Shopware\Core\PlatformRequest;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\User\UserEntity;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\KernelEvents;
use Iwv\IwvTwoFactorAuthentication\Service\TwoFactorAdaptors\GoogleAuthenticatorAdaptor;
use Iwv\IwvTwoFactorAuthentication\Service\TwoFactorAdaptors\YubicoAuthenticatorAdaptor;
use Iwv\IwvTwoFactorAuthentication\Service\TwoFactorAdaptors\EmailAuthenticatorAdaptor;
use Iwv\IwvTwoFactorAuthentication\Service\CookieHelperValidator;
use Exception;
class BackendLoginSubscriber implements EventSubscriberInterface
{
/**
* @var EntityRepositoryInterface
*/
private $userRepository;
/**
* @var CookieHelperValidator
*/
private $cookieHelperValidator;
/**
* @var GoogleAuthenticatorAdaptor
*/
private $googleAuthenticatorAdaptor;
/**
* @var YubicoAuthenticatorAdaptor
*/
private $yubicoAuthenticatorAdaptor;
/**
* @var EmailAuthenticatorAdaptor
*/
private $emailAuthenticatorAdaptor;
public function __construct(
EntityRepositoryInterface $userRepository,
CookieHelperValidator $cookieHelperValidator,
GoogleAuthenticatorAdaptor $googleAuthenticatorAdaptor,
YubicoAuthenticatorAdaptor $yubicoAuthenticatorAdaptor,
EmailAuthenticatorAdaptor $emailAuthenticatorAdaptor
) {
$this->userRepository = $userRepository;
$this->cookieHelperValidator = $cookieHelperValidator;
$this->googleAuthenticatorAdaptor = $googleAuthenticatorAdaptor;
$this->yubicoAuthenticatorAdaptor = $yubicoAuthenticatorAdaptor;
$this->emailAuthenticatorAdaptor = $emailAuthenticatorAdaptor;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onResponse',
];
}
public function onResponse(ResponseEvent $event): void
{
/** @var \Symfony\Component\HttpFoundation\Request $request */
$request = $event->getRequest();
/** @var \Symfony\Component\HttpFoundation\JsonResponse $response */
$response = $event->getResponse();
/* return on invalid requests */
if (!in_array($event->getResponse()->getStatusCode(), [200, 204])) {
return;
}
/* $event->getContext() does not exists */
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT);
/* update cookie request on profiles change */
if (
$request->attributes->get('_route') === 'api.action.iwv.two-factor.backend.google-authenticator' ||
$request->attributes->get('_route') === 'api.action.iwv.two-factor.backend.yubico-configurator' ||
$request->attributes->get('_route') === 'api.action.iwv.two-factor.backend.email-validate' ||
$request->attributes->get('_route') === 'api.action.iwv.two-factor.backend.disable-validator'
) {
$responseContent = json_decode($response->getContent(), true);
if (!array_key_exists('status', $responseContent['response']) || !$responseContent['response']['status']) {
return;
}
if ($responseContent['response']['userId']) {
$userDB = $context->scope(Context::SYSTEM_SCOPE, fn(Context $systemContext) => $this->userRepository->search(new Criteria([$responseContent['response']['userId']]), $systemContext)->first());
$customFields = $userDB->getCustomFields();
$twoFaParams = $customFields['iwvTwoFactor'] ?? [];
$response = !empty($twoFaParams) ? $this->cookieHelperValidator->addTwoFaCookie($response, $userDB) : $this->cookieHelperValidator->removeTwoFaCookie($response);
$event->setResponse($response);
}
return;
}
if ($request->attributes->get('_route') !== 'api.oauth.token') {
return;
}
$username = $request->request->get('username');
/** @var UserEntity $userDB */
$userDB = $context->scope(Context::SYSTEM_SCOPE, fn(Context $systemContext) => $this->userRepository->search((new Criteria())->addFilter(new EqualsFilter('username', $username)), $systemContext)->first());
if (!$userDB || !($customFields = $userDB->getCustomFields()) || !isset($customFields['iwvTwoFactor']) || empty($customFields['iwvTwoFactor']) || ($this->cookieHelperValidator->hasScope($request, \Shopware\Core\Framework\Api\OAuth\Scope\UserVerifiedScope::IDENTIFIER) && $this->cookieHelperValidator->hasTwoFaCookie($request, $userDB))) {
return;
}
$context->scope(Context::SYSTEM_SCOPE, fn(Context $systemContext) => $this->emailAuthenticatorAdaptor->load($systemContext, $userDB));
try {
$authenticatorCode = (string) $request->request->get('iwvAuthenticator');
$authenticatorData = (array) $request->request->get('iwvAuthenticationToken');
$context->scope(Context::SYSTEM_SCOPE, fn(Context $systemContext) => $this->emailAuthenticatorAdaptor->load($systemContext, $userDB));
if (!isset($authenticatorCode)) {
throw new Exception('OTP is required', 1200);
} elseif (!isset($customFields['iwvTwoFactor'][$authenticatorCode])) {
throw new Exception('Authenticator is not configured', 1200);
}
switch ($authenticatorCode) {
case 'emailAuth';
if($authenticatorData['sendCodeButton']){
$this->emailAuthenticatorAdaptor->sendLoginEmailCode();
throw new Exception('Email has been sent', 1203);
}
else{
$emailData = $this->emailAuthenticatorAdaptor->authenticate(($authenticatorData['otpValue'] ?? ''));
if(!$emailData['verified']){
if($emailData['error'] === 'expired'){
throw new Exception('Invalid authentication code', 1201);
}
else{
throw new Exception('Authentication code expired', 1201);
}
}
}
break;
case 'yubico';
if (!$this->yubicoAuthenticatorAdaptor->authenticate($customFields['iwvTwoFactor']['yubico']['clientId'], $customFields['iwvTwoFactor']['yubico']['secret'], ($authenticatorData['otpValue'] ?? ''))) {
throw new Exception('Invalid authentication code', 1201);
}
break;
case 'otp2fa';
if (!$this->googleAuthenticatorAdaptor->authenticate($customFields['iwvTwoFactor']['otp2fa']['secret'], ($authenticatorData['otpValue'] ?? ''))) {
throw new Exception('Invalid authentication code', 1201);
}
break;
}
$response = $this->cookieHelperValidator->addTwoFaCookie($response, $userDB);
$event->setResponse($response);
} catch (\Exception $ex) {
throw new OAuthServerException('OTP is required', $ex->getCode(), 'iwv-request-otp', 401, array_keys($customFields['iwvTwoFactor']));
}
}
}