app/Plugin/JsysAsi42/Event.php line 265

Open in your IDE?
  1. <?php
  2. namespace Plugin\JsysAsi42;
  3. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  4. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  5. use Symfony\Component\HttpFoundation\RequestStack;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Eccube\Entity\Member;
  8. use Eccube\Event\EccubeEvents;
  9. use Eccube\Event\EventArgs;
  10. use Eccube\Event\TemplateEvent;
  11. use Eccube\Repository\MemberRepository;
  12. use Plugin\JsysAsi42\Entity\Config;
  13. use Plugin\JsysAsi42\Entity\JsysAsiLoginHistoryLockStatus;
  14. use Plugin\JsysAsi42\Entity\JsysAsiLoginHistoryStatus;
  15. use Plugin\JsysAsi42\Entity\JsysAsiLoginHistoryTfaStatus;
  16. use Plugin\JsysAsi42\Repository\ConfigRepository;
  17. use Plugin\JsysAsi42\Repository\JsysAsiTfaUserRepository;
  18. use Plugin\JsysAsi42\Repository\JsysAsiTfaOtpHistoryRepository;
  19. use Plugin\JsysAsi42\Repository\JsysAsiLoginHistoryRepository;
  20. use Plugin\JsysAsi42\Service\JsysAsiTfaService;
  21. use Plugin\JsysAsi42\Service\JsysAsiNotificationService;
  22. use Plugin\JsysAsi42\Service\JsysAsiLockService;
  23. use Doctrine\ORM\Query\Expr\Join;
  24. use Eccube\Request\Context;
  25. use Plugin\JsysAsi42\Security\Http\Authenticator\Passport\Badge\JsysAsiAuthBadge;
  26. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  27. use Symfony\Component\Security\Http\Event\CheckPassportEvent;
  28. use Symfony\Component\Security\Http\Event\LoginFailureEvent;
  29. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  30. use Twig\Environment;
  31. class Event implements EventSubscriberInterface
  32. {
  33.     /**
  34.      * @var EntityManagerInterface
  35.      */
  36.     private $entityManager;
  37.     /**
  38.      * @var Environment
  39.      */
  40.     private $twig;
  41.     /**
  42.      * @var RequestStack
  43.      */
  44.     private $requestStack;
  45.     /**
  46.      * @var Context
  47.      */
  48.     private $requestContext;
  49.     /**
  50.      * @var JsysAsiTfaService
  51.      */
  52.     private $tfaService;
  53.     /**
  54.      * @var JsysAsiLockService
  55.      */
  56.     private $lockService;
  57.     /**
  58.      * @var JsysAsiNotificationService
  59.      */
  60.     private $notifyService;
  61.     /**
  62.      * @var MemberRepository
  63.      */
  64.     private $memberRepo;
  65.     /**
  66.      * @var JsysAsiTfaUserRepository
  67.      */
  68.     private $tfaUserRepo;
  69.     /**
  70.      * @var JsysAsiTfaOtpHistoryRepository
  71.      */
  72.     private $otpHistoryRepo;
  73.     /**
  74.      * @var JsysAsiLoginHistoryRepository
  75.      */
  76.     private $loginHistoryRepo;
  77.     /**
  78.      * @var Config
  79.      */
  80.     private $Config;
  81.     /**
  82.      * JsysAsi Event constructor.
  83.      * @param EntityManagerInterface $entityManager
  84.      * @param Environment $twig
  85.      * @param RequestStack $requestStack
  86.      * @param Context $requestContext
  87.      * @param JsysAsiTfaService $tfaService
  88.      * @param JsysAsiLockService $lockService
  89.      * @param JsysAsiNotificationService $notifyService
  90.      * @param ConfigRepository $configRepo
  91.      * @param MemberRepository $memberRepo
  92.      * @param JsysAsiTfaUserRepository $tfaUserRepo
  93.      * @param JsysAsiTfaOtpHistoryRepository $otpHistoryRepo
  94.      * @param JsysAsiLoginHistoryRepository $loginHistoryRepo
  95.      */
  96.     public function __construct(
  97.         EntityManagerInterface $entityManager,
  98.         Environment $twig,
  99.         RequestStack $requestStack,
  100.         Context $requestContext,
  101.         JsysAsiTfaService $tfaService,
  102.         JsysAsiLockService $lockService,
  103.         JsysAsiNotificationService $notifyService,
  104.         ConfigRepository $configRepo,
  105.         MemberRepository $memberRepo,
  106.         JsysAsiTfaUserRepository $tfaUserRepo,
  107.         JsysAsiTfaOtpHistoryRepository $otpHistoryRepo,
  108.         JsysAsiLoginHistoryRepository $loginHistoryRepo
  109.     ) {
  110.         $this->entityManager    $entityManager;
  111.         $this->twig             $twig;
  112.         $this->requestStack     $requestStack;
  113.         $this->requestContext   $requestContext;
  114.         $this->tfaService       $tfaService;
  115.         $this->lockService      $lockService;
  116.         $this->notifyService    $notifyService;
  117.         $this->memberRepo       $memberRepo;
  118.         $this->tfaUserRepo      $tfaUserRepo;
  119.         $this->otpHistoryRepo   $otpHistoryRepo;
  120.         $this->loginHistoryRepo $loginHistoryRepo;
  121.         $this->Config           $configRepo->get();
  122.     }
  123.     /**
  124.      * @return array
  125.      */
  126.     public static function getSubscribedEvents()
  127.     {
  128.         return [
  129.             CheckPassportEvent::class                                 => ['onCheckPassport', -10],
  130.             LoginSuccessEvent::class                                  => 'onLoginSuccess',
  131.             LoginFailureEvent::class                                  => 'onLoginFailure',
  132.             EccubeEvents::ADMIN_SETTING_SYSTEM_MEMBER_DELETE_COMPLETE => 'onAdminMemberDeleteComplete',
  133.             '@admin/login.twig'                                       => 'onAdminRenderLogin',
  134.         ];
  135.     }
  136.     /**
  137.      * パスポートチェック
  138.      * 優先順位の指定によりログイン成功判定後に実行、ログイン失敗時は実行されない
  139.      *
  140.      * @param CheckPassportEvent $event
  141.      * @return void
  142.      */
  143.     public function onCheckPassport(CheckPassportEvent $event)
  144.     {
  145.         // 管理画面のみ実行
  146.         if (!$this->requestContext->isAdmin()) {
  147.             return;
  148.         }
  149.         // ログインユーザーを取得
  150.         $passport $event->getPassport();
  151.         $user     null;
  152.         if ($passport->hasBadge(UserBadge::class)) {
  153.             /** @var UserBadge $badge */
  154.             $badge  $passport->getBadge(UserBadge::class);
  155.             $user   $badge->getUser();
  156.         }
  157.         if (!$user instanceof Member) {
  158.             return;
  159.         }
  160.         // リクエストからパラメータ取得
  161.         $request $this->requestStack->getMainRequest();
  162.         $ip      $request->getClientIp();
  163.         $code    $request->request->get('jsys_asi_otp');
  164.         // 2要素認証コード・IPアドレスロックをチェックし結果ステータスを取得
  165.         $tfa_status  $this->getTfaStatusCode($user$code);
  166.         $lock_status $this->getLockStatusCode($ip);
  167.         // チェックに失敗した場合はステータスに拒否を代入
  168.         $status JsysAsiLoginHistoryStatus::SUCCESS;
  169.         if (
  170.             JsysAsiLoginHistoryTfaStatus::USED == $tfa_status
  171.             || JsysAsiLoginHistoryTfaStatus::INVALID == $tfa_status
  172.             || JsysAsiLoginHistoryLockStatus::LOCKED == $lock_status
  173.         ) {
  174.             $status JsysAsiLoginHistoryStatus::DENY;
  175.         }
  176.         // バッジを作成、ステータスが成功の場合は解決済にする
  177.         $badge = new JsysAsiAuthBadge($status$tfa_status$lock_status);
  178.         if (JsysAsiLoginHistoryStatus::SUCCESS == $status) {
  179.             $badge->markResolved();
  180.         }
  181.         // パスポートへ設定
  182.         $passport->addBadge($badge);
  183.         // ステータスが成功以外の場合は認証失敗例外を発生
  184.         if (JsysAsiLoginHistoryStatus::SUCCESS != $status) {
  185.             throw new CustomUserMessageAuthenticationException('Invalid credentials.');
  186.         }
  187.     }
  188.     /**
  189.      * ログイン成功
  190.      *
  191.      * @param LoginSuccessEvent $event
  192.      * @return void
  193.      */
  194.     public function onLoginSuccess(LoginSuccessEvent $event)
  195.     {
  196.         // 管理画面のみ実行
  197.         if (!$this->requestContext->isAdmin()) {
  198.             return;
  199.         }
  200.         // ログインユーザーを取得
  201.         $passport $event->getPassport();
  202.         $user     null;
  203.         if ($passport->hasBadge(UserBadge::class)) {
  204.             /** @var UserBadge $badge */
  205.             $badge  $passport->getBadge(UserBadge::class);
  206.             $user   $badge->getUser();
  207.         }
  208.         if (!$user instanceof Member) {
  209.             return;
  210.         }
  211.         // 認証バッジが無ければ終了
  212.         if (!$passport->hasBadge(JsysAsiAuthBadge::class)) {
  213.             return;
  214.         }
  215.         /** @var JsysAsiAuthBadge $auth_badge */
  216.         $auth_badge $passport->getBadge(JsysAsiAuthBadge::class);
  217.         // ログイン成功をログイン履歴へ保存
  218.         $this->loginHistoryRepo->saveHistory(
  219.             $user->getLoginId(),
  220.             $user->getLoginDate(),
  221.             $auth_badge->getStatus(),
  222.             $auth_badge->getTfaStatus(),
  223.             $auth_badge->getLockStatus()
  224.         );
  225.         // ログイン成功通知が有効ならログイン成功メールを送信
  226.         if ($this->Config->getOptionLoginSuccessMail()) {
  227.             $this->notifyService->sendLoginSuccessMail($user);
  228.         }
  229.     }
  230.     /**
  231.      * ログイン失敗
  232.      *
  233.      * @param LoginFailureEvent $event
  234.      * @return void
  235.      */
  236.     public function onLoginFailure(LoginFailureEvent $event)
  237.     {
  238.         // 管理画面のみ実行
  239.         if (!$this->requestContext->isAdmin()) {
  240.             return;
  241.         }
  242.         // リクエストからパラメータ取得
  243.         $request $this->requestStack->getMainRequest();
  244.         $ip      $request->getClientIp();
  245.         $code    $request->request->get('jsys_asi_otp');
  246.         // ログインIDとログインIDと一致するユーザーを取得
  247.         $passport $event->getPassport();
  248.         $login_id '';
  249.         $user     null;
  250.         if ($passport->hasBadge(UserBadge::class)) {
  251.             /** @var UserBadge $badge */
  252.             $badge    $passport->getBadge(UserBadge::class);
  253.             $login_id $badge->getUserIdentifier();
  254.             /** @var Member $user */
  255.             $user     $this->memberRepo->findOneBy(['login_id' => $login_id]);
  256.         }
  257.         // 認証バッジがあればバッジから結果を取得、無ければチェックして結果を取得
  258.         $status      JsysAsiLoginHistoryStatus::FAILURE;
  259.         $tfa_status  JsysAsiLoginHistoryTfaStatus::DISABLED;
  260.         $lock_status JsysAsiLoginHistoryLockStatus::DISABLED;
  261.         if ($passport->hasBadge(JsysAsiAuthBadge::class)) {
  262.             /** @var JsysAsiAuthBadge $auth_badge  */
  263.             $auth_badge  $passport->getBadge(JsysAsiAuthBadge::class);
  264.             $status      $auth_badge->getStatus();
  265.             $tfa_status  $auth_badge->getTfaStatus();
  266.             $lock_status $auth_badge->getLockStatus();
  267.         } else {
  268.             // 2要素認証コードとIPアドレスロックをチェック
  269.             if ($user) {
  270.                 $tfa_status $this->getTfaStatusCode($user$code);
  271.             }
  272.             $lock_status $this->getLockStatusCode($ip);
  273.         }
  274.         // 2要素とロックチェックに問題があればlog_infoで保存
  275.         $msg 'JsysAsi 管理 ログイン' JsysAsiLoginHistoryStatus::DENY '拒否' '失敗';
  276.         if (
  277.             JsysAsiLoginHistoryTfaStatus::USED == $tfa_status
  278.             || JsysAsiLoginHistoryTfaStatus::INVALID == $tfa_status
  279.         ) {
  280.             log_info($msg ' 不正認証コード', [
  281.                 'member_id' => $user->getId(),
  282.                 'code'      => $code,
  283.             ]);
  284.         }
  285.         if (JsysAsiLoginHistoryLockStatus::LOCKED == $lock_status) {
  286.             log_info($msg ' ロック済みIPアドレス', [
  287.                 'login_id'   => $login_id,
  288.                 'ip_address' => $ip,
  289.             ]);
  290.         }
  291.         // ログイン失敗・拒否をログイン履歴へ保存
  292.         $this->loginHistoryRepo->saveHistory(
  293.             $login_id,
  294.             new \DateTime(),
  295.             $status,
  296.             $tfa_status,
  297.             $lock_status
  298.         );
  299.         // ログインに連続で失敗している場合はIPアドレスをロック
  300.         if($this->lockService->shouldLockIpAddress($ip)) {
  301.             $this->lockService->lockIpAddress($ip);
  302.         }
  303.         // ログイン失敗通知が有効ならログイン失敗メールを送信
  304.         if ($this->Config->getOptionLoginFailureMail()) {
  305.             $this->notifyService->sendLoginFailureMail($login_id);
  306.         }
  307.     }
  308.     /**
  309.      * 管理 > 設定 > システム設定 > メンバー管理 > 削除
  310.      *  - 2要素認証ユーザーとOTP履歴を削除
  311.      * @param EventArgs $event
  312.      */
  313.     public function onAdminMemberDeleteComplete(EventArgs $event)
  314.     {
  315.         // メンバーが存在しない2要素認証ユーザーを全て取得
  316.         $tfaUsers $this->tfaUserRepo->createQueryBuilder('u')
  317.             ->select()
  318.             ->leftJoin(Member::class, 'm'Join::WITH'm.id = u.member_id')
  319.             ->where('m.id IS NULL')
  320.             ->getQuery()
  321.             ->getResult();
  322.         // 削除されたメンバーのOTP履歴と2要素認証ユーザーを削除
  323.         foreach ($tfaUsers as $tfaUser) {
  324.             $this->otpHistoryRepo->deleteByTfaUser($tfaUser);
  325.             $this->entityManager->remove($tfaUser);
  326.             $this->entityManager->flush($tfaUser);
  327.             log_info('JsysAsi 2要素認証ユーザー 削除', [ 'JsysAsiTfaUser' => $tfaUser]);
  328.         }
  329.     }
  330.     /**
  331.      * 管理 > ログイン画面
  332.      *  - 2要素認証有効時にOTPを追加
  333.      * @param TemplateEvent $event
  334.      */
  335.     public function onAdminRenderLogin(TemplateEvent $event)
  336.     {
  337.         // 2要素認証が有効なら認証コードを画面に追加
  338.         if ($this->Config->getOptionTfa()) {
  339.             $this->addOtpTwigToAdminLogin($event);
  340.         }
  341.     }
  342.     /**
  343.      * 管理ログイン画面に認証コードを追加します。
  344.      * @param TemplateEvent $event
  345.      */
  346.     private function addOtpTwigToAdminLogin(TemplateEvent $event)
  347.     {
  348.         /*
  349.          * admin/login.twigにsnippet.twigのincludeがないため、ソースへ直接追加
  350.          */
  351.         $tpl    '@JsysAsi42/admin/login_ext_otp.twig';
  352.         $otp    $this->twig->getLoader()->getSourceContext($tpl)->getCode();
  353.         $search '{% if error %}';
  354.         $event->setSource(str_replace($search$otp $search$event->getSource()));
  355.     }
  356.     /**
  357.      * 2要素認証コードをチェックし、結果コードを取得します。
  358.      *
  359.      * @param Member $user
  360.      * @param string $code
  361.      * @return int
  362.      */
  363.     private function getTfaStatusCode($user$code): int
  364.     {
  365.         // 2要素認証機能無効
  366.         if (!$this->Config->getOptionTfa()) {
  367.             return JsysAsiLoginHistoryTfaStatus::DISABLED
  368.         }
  369.         // 有効なコード
  370.         if ($this->tfaService->isValidCode($user$code)) {
  371.             return JsysAsiLoginHistoryTfaStatus::VALID;
  372.         }
  373.         // 使用済みコード または 無効なコード
  374.         $result $this->tfaService->getTfaResult();
  375.         if (JsysAsiTfaService::RESULT_TFA_USED == $result) {
  376.             return JsysAsiLoginHistoryTfaStatus::USED;
  377.         }
  378.         return JsysAsiLoginHistoryTfaStatus::INVALID;
  379.     }
  380.     /**
  381.      * IPアドレスロックをチェックし、結果コードを取得します。
  382.      *
  383.      * @param string $ip
  384.      * @return int
  385.      */
  386.     private function getLockStatusCode($ip): int
  387.     {
  388.         // IPアドレスロック機能無効
  389.         if (!$this->Config->getOptionIpAddressLock()) {
  390.             return JsysAsiLoginHistoryLockStatus::DISABLED;
  391.         }
  392.         // ロック済み
  393.         if ($this->lockService->isLockedIpAddress($ip)) {
  394.             return JsysAsiLoginHistoryLockStatus::LOCKED;
  395.         }
  396.         // 未ロック
  397.         return JsysAsiLoginHistoryLockStatus::UNLOCKED;
  398.     }
  399. }