<?php
namespace Plugin\ApgEnhanceSecurityOfAdmin;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Eccube\Application;
use Eccube\Common\EccubeConfig;
use Eccube\Entity\Master\Work;
use Eccube\Entity\Member;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Event\TemplateEvent;
use Eccube\Repository\MemberRepository;
use Eccube\Request\Context;
use Plugin\ApgEnhanceSecurityOfAdmin\Domain\OnetimeType;
use Plugin\ApgEnhanceSecurityOfAdmin\Entity\ApgLoginHistory;
use Plugin\ApgEnhanceSecurityOfAdmin\Entity\ApgOperationHistory;
use Plugin\ApgEnhanceSecurityOfAdmin\Entity\Config;
use Plugin\ApgEnhanceSecurityOfAdmin\Repository\ConfigRepository;
use Plugin\ApgEnhanceSecurityOfAdmin\Repository\LoginHistoryRepository;
use Plugin\ApgEnhanceSecurityOfAdmin\Repository\OperationHistoryRepository;
use Plugin\ApgEnhanceSecurityOfAdmin\Service\MailService;
use Plugin\ApgEnhanceSecurityOfAdmin\Service\TwoFactorService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class Event implements EventSubscriberInterface
{
/**
* @var EntityManager
*/
protected $em;
/** @var Context $requestContext */
protected $requestContext;
/** @var EccubeConfig $eccubeConfig */
protected $eccubeConfig;
/** @var RequestStack $requestStack */
protected $requestStack;
/** @var ConfigRepository $configRepository */
protected $configRepository;
/** @var MemberRepository $memberRepository */
protected $memberRepository;
/** @var LoginHistoryRepository */
protected $loginHistoryRepository;
/** @var OperationHistoryRepository */
protected $operationHistoryRepository;
/** @var UrlGeneratorInterface $generator */
protected $generator;
/** @var TokenStorageInterface $tokenStorage */
protected $tokenStorage;
/** @var MailService */
protected $mailService;
/** @var TwoFactorService */
protected $twoFactorService;
/** @var \Twig_Environment */
protected $twig;
const TEMPLATE_NAMESPACE = '@ApgEnhanceSecurityOfAdmin';
const SESSION_ONETIME_PASSWORD_CODE = "apg_onetime_password_code";
const SESSION_ONETIME_VALID = "apg_onetime_valid";
public function __construct(
EntityManagerInterface $em
, Context $context
, \Twig_Environment $twig
, RequestStack $requestStack
, EccubeConfig $eccubeConfig
, ConfigRepository $configRepository
, LoginHistoryRepository $loginHistoryRepository
, MemberRepository $memberRepository
, OperationHistoryRepository $operationRepository
, UrlGeneratorInterface $generator
, TokenStorageInterface $tokenStorage
, MailService $mailService
, TwoFactorService $twoFactorService
)
{
$this->em = $em;
$this->twig = $twig;
$this->requestStack = $requestStack;
$this->eccubeConfig = $eccubeConfig;
$this->requestContext = $context;
$this->configRepository = $configRepository;
$this->loginHistoryRepository = $loginHistoryRepository;
$this->memberRepository = $memberRepository;
$this->operationHistoryRepository = $operationRepository;
$this->generator = $generator;
$this->tokenStorage = $tokenStorage;
$this->mailService = $mailService;
$this->twoFactorService = $twoFactorService;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
KernelEvents::REQUEST => 'onKernelRequest',
EccubeEvents::ADMIN_ADMIM_INDEX_COMPLETE => 'onAdminAdminIndexComplete',
EccubeEvents::ADMIN_SETTING_SYSTEM_MEMBER_EDIT_INITIALIZE => 'onAdminSettingSystemMemberEditInitialize',
EccubeEvents::ADMIN_ADMIN_CHANGE_PASSWORD_COMPLETE => 'onAdminAdminChangePasswordComplete',
'@admin/Setting/System/member.twig' => 'onRenderAdminSettingSystemMember',
];
}
public function onKernelRequest(GetResponseEvent $event)
{
$app = Application::getInstance();
// $security = $app->getParentContainer()->get('security');
if (!$event->isMasterRequest()) {
return;
}
if ($this->requestContext->isAdmin()) {
$config = $this->configRepository->getOrNew();
/** @var Request $request */
$request = $event->getRequest();
$requestUrl = $request->getRequestUri();
$loginUrl = $this->generator->generate('admin_login');
$onetimeUrl = $this->generator->generate('apg_enhance_security_of_admin_onetime');
/** @var Member $user */
$member = $this->requestContext->getCurrentUser();
if (!empty($member)) {
if (!empty($config->getUseOperationHistory())) {
// 操作ログの保存
$operationHistory = new ApgOperationHistory();
$operationHistory->setMetaFromServer($request);
$operationHistory->setLoginId($member->getLoginId());
$operationHistory->setMemberId($member->getId());
$operationHistory->setMemberName($member->getName());
$operationHistory->setAction($request->getMethod());
$this->operationHistoryRepository->save($operationHistory);
$this->em->flush();
}
}
if (!empty($config->getUseOnetime())) {
if (
strpos($requestUrl, $loginUrl) !== 0
&& strpos($requestUrl, $onetimeUrl) !== 0
) {
if ($request->getSession()->has(self::SESSION_ONETIME_VALID)) {
$pass = $request->getSession()->get(self::SESSION_ONETIME_VALID);
if (empty($pass)) {
$event->setResponse(new RedirectResponse($onetimeUrl));
}
} else {
// ログアウト処理
$this->tokenStorage->setToken(null);
$request->getSession()->clear();
$event->setResponse(new RedirectResponse($loginUrl));
}
}
}
}
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
/** @var Request $request */
$request = $event->getRequest();
$user = $event
->getAuthenticationToken()
->getUser();
if ($user instanceof Member) {
$config = $this->configRepository->getOrNew();
Request::setTrustedProxies(
['192.0.0.1', '10.0.0.0/8'],
// trust *all* "X-Forwarded-*" headers
Request::HEADER_X_FORWARDED_ALL
// // or, if your proxy instead uses the "Forwarded" header
// Request::HEADER_FORWARDED
);
// @todo ロードバランサー対応(将来?)
// Request::setTrustedProxies(
// // trust *all* requests
// ['127.0.0.1', $request->server->get('REMOTE_ADDR')],
//
// // if you're using ELB, otherwise use a constant from above
// Request::HEADER_X_FORWARDED_AWS_ELB
// );
$loginHistory = $this->loginHistoryRepository->createLoginHistory($request, $user);
if ($loginId = $request->get('login_id', null)) {
$loginHistory->setInputLoginId($loginId);
}
if ($config->isLocked($user)) {
// ロックカウントをオーバーしていたら、ログインに成功していても弾く
$locked = true;
$loginHistory->setLocked();
} else {
$locked = false;
// 2段階認証用のセッションを保存
$useOnetime = false;
if (!empty($config->getUseOnetime())) {
if (!empty($user->getUseOnetime())) {
$useOnetime = true;
}
}
$this->updateFailureCount($config, $user, 0);
if (!$useOnetime) {
$loginHistory->setSuccess();
$request->getSession()->set(self::SESSION_ONETIME_VALID, true);
} else {
$loginHistory->setSuccess();
$request->getSession()->set(self::SESSION_ONETIME_VALID, false);
$targetDate = new \DateTime();
$expireTime = $config->getOnetimeExpireTime();
if (empty($expireTime) || $expireTime < 0) {
$expireTime = 10; // デフォルト10分
}
if ($config->getOnetimeType() === OnetimeType::EMAIL) {
$aryOnetime = $this->twoFactorService->createOnetime($targetDate, $expireTime);
$this->mailService->sendOnetimePasswordMail($user, $aryOnetime['onetime'], $aryOnetime['expireDate']);
$request->getSession()->set(self::SESSION_ONETIME_PASSWORD_CODE, $aryOnetime);
}
}
}
if (!empty($config->getUseLoginHistory())) {
$this->loginHistoryRepository->save($loginHistory);
}
$this->em->flush();
if ($locked) {
throw new LockedException();
}
if (!$useOnetime) { // 2段階認証利用外のときだけ
if (!empty($config->getUseLoginEmail())) { // ログインメールが有効のときだけ
$loginHistory = $this->loginHistoryRepository->createLoginHistory($request, $user);
$this->mailService->sendLoginMail($user, $loginHistory);
}
}
$this->em->flush();
}
}
public function onAuthenticationFailure(AuthenticationFailureEvent $event)
{
/** @var UsernamePasswordToken $token */
$token = $event->getAuthenticationToken();
if ($token->getProviderKey() === 'admin') {
$locked = false;
$request = $this->requestStack->getCurrentRequest();
$loginHistory = new ApgLoginHistory();
$loginHistory->setMetaFromServer($request);
$loginHistory->setFailure();
if ($loginId = $request->get('login_id', null)) {
$loginHistory->setInputLoginId($loginId);
$user = $this->getMember($loginId);
if (!empty($user)) {
$loginHistory->setMemberId($user->getId());
$loginHistory->setMemberName($user->getName());
$loginHistory->setMemberEmail($user->getNotifyEmail());
$config = $this->configRepository->getOrNew();
if ($config->isLocked($user)) {
// ロックカウントをオーバーしていたら、ロック用のメッセージを出力
$locked = true;
$loginHistory->setLocked();
} else {
$user = $this->updateFailureCount($config, $user, 1);
if ($config->isLocked($user)) {
// 最初にロックされた段階でメールを飛ばす
$this->mailService->sendAccountLockedMail($user, $loginHistory);
}
}
}
}
$this->loginHistoryRepository->save($loginHistory);
$this->em->flush();
if ($locked) {
throw new LockedException();
}
}
}
private function getMember($loginId)
{
/** @var Member $member */
$member = $this->memberRepository->findOneBy(['login_id' => $loginId, 'Work' => Work::ACTIVE]);
return $member;
}
/**
* @param Member $member
* @param int $count
* @return Member
*/
private function updateFailureCount(Config $config, Member $member, $count = 0)
{
if ($count > 0) {
$storedCount = $member->getLoginFailureCount();
if (empty($storedCount) || $config->isFailureExpired($member)) {
// 最終ログイン失敗日時が有効期限切れの場合は、失敗回数を0に戻す
$storedCount = 0;
}
$count = $storedCount + $count;
$member->setLoginFailureLastDate(new \DateTime());
} else {
$count = 0;
$member->setLoginFailureLastDate(null);
}
$member->setLoginFailureCount($count);
$this->em->persist($member);
return $member;
}
public function onAdminAdminIndexComplete(EventArgs $event)
{
/** @var Request $request */
$request = $event->getRequest();
/** @var Member $user */
$member = $this->requestContext->getCurrentUser();
if (empty($member->getNotifyEmail()) && !$request->isXmlHttpRequest()) {
$request->getSession()->getFlashBag()->add('eccube.admin.danger', 'メールアドレスが登録されていません。[登録情報]の[アカウント編集]よりメールアドレスを登録してください。');
}
}
public function onAdminSettingSystemMemberEditInitialize(EventArgs $event)
{
}
public function onAdminAdminChangePasswordComplete(EventArgs $event)
{
$config = $this->configRepository->get();
if (!empty($config->getUseEditEmail())) {
/** @var Request $request */
$request = $event->getRequest();
/** @var Member $member */
$member = $event->getArgument('Member');
$operationHistory = new ApgOperationHistory();
$operationHistory->setMetaFromServer($request);
$operationHistory->setLoginId($member->getLoginId());
$this->mailService->sendEditPasswordMail($member, $operationHistory);
}
}
public function onRenderAdminSettingSystemMember(TemplateEvent $event)
{
$source = $event->getSource();
// data
$parameters = $event->getParameters();
// setting
$loader = $this->twig->getLoader();
// header
$pattern = '|<th class="border-top-0 pt-2 pb-2 text-center"></th>|s';
$addRow = '<th class="border-top-0 pt-2 pb-2 text-center">メールアドレス</th><th class="border-top-0 pt-2 pb-2 text-center">2段階認証</th>';
if (preg_match($pattern, $source, $matches, PREG_OFFSET_CAPTURE)) {
$replacement = $addRow . $matches[0][0];
$source = preg_replace($pattern, $replacement, $source);
}
// data
$pattern = '|{{ Member.Work.name }}(.*?)</td>|s';
$addRow = $this->twig->getLoader()->getSourceContext(self::TEMPLATE_NAMESPACE . '/admin/setting_system_member.twig')->getCode();
if (preg_match($pattern, $source, $matches, PREG_OFFSET_CAPTURE)) {
$replacement = $matches[0][0] . $addRow;
$source = preg_replace($pattern, $replacement, $source);
}
$event->setSource($source);
$event->setParameters($parameters);
}
}