410 lines
12 KiB
PHP
410 lines
12 KiB
PHP
<?php
|
||
|
||
/*
|
||
* This file is part of EC-CUBE
|
||
*
|
||
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
|
||
*
|
||
* http://www.ec-cube.co.jp/
|
||
*
|
||
* For the full copyright and license information, please view the LICENSE
|
||
* file that was distributed with this source code.
|
||
*/
|
||
|
||
namespace Plugin\Passkeys\Service;
|
||
|
||
use Doctrine\ORM\EntityManagerInterface;
|
||
use Eccube\Common\EccubeConfig;
|
||
use Eccube\Entity\BaseInfo;
|
||
use Eccube\Entity\Customer;
|
||
use Eccube\Repository\BaseInfoRepository;
|
||
use Plugin\Passkeys\Entity\PasskeysCustomerCookie;
|
||
use Plugin\Passkeys\Repository\PasskeysAuthConfigRepository;
|
||
use Plugin\Passkeys\Repository\PasskeysAuthCustomerCookieRepository;
|
||
use Psr\Container\ContainerInterface;
|
||
use Symfony\Component\HttpFoundation\Cookie;
|
||
use Symfony\Component\HttpFoundation\Request;
|
||
use Symfony\Component\HttpFoundation\RequestStack;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
|
||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||
use Twilio\Exceptions\ConfigurationException;
|
||
use Twilio\Rest\Client;
|
||
|
||
class CustomerPasskeysAuthService
|
||
{
|
||
/**
|
||
* @var string コールバックURL
|
||
*/
|
||
public const SESSION_CALL_BACK_URL = 'plugin_eccube_customer_passkeys_call_back_url';
|
||
|
||
/**
|
||
* @var ContainerInterface
|
||
*/
|
||
protected $container;
|
||
/**
|
||
* @var EccubeConfig
|
||
*/
|
||
protected $eccubeConfig;
|
||
/**
|
||
* @var EncoderFactoryInterface
|
||
*/
|
||
protected $encoderFactory;
|
||
/**
|
||
* @var RequestStack
|
||
*/
|
||
protected $requestStack;
|
||
/**
|
||
* @var Request
|
||
*/
|
||
protected $request;
|
||
/**
|
||
* @var string
|
||
*/
|
||
protected $cookieName;
|
||
/**
|
||
* @var string
|
||
*/
|
||
protected $routeCookieName;
|
||
/**
|
||
* @var int
|
||
*/
|
||
protected $expire;
|
||
/**
|
||
* @var int
|
||
*/
|
||
protected $route_expire;
|
||
/**
|
||
* @var EntityManagerInterface
|
||
*/
|
||
private $entityManager;
|
||
/**
|
||
* @var BaseInfo|object|null
|
||
*/
|
||
private $baseInfo;
|
||
|
||
/**
|
||
* @var PasskeysAuthConfig
|
||
*/
|
||
private $PasskeysAuthConfig;
|
||
|
||
/**
|
||
* @var array
|
||
*/
|
||
private $default_tfa_routes = [
|
||
'login',
|
||
'mypage_login',
|
||
'mypage',
|
||
'mypage_order',
|
||
'mypage_favorite',
|
||
'mypage_change',
|
||
'mypage_delivery',
|
||
'mypage_withdraw',
|
||
'shopping',
|
||
'shopping_login',
|
||
];
|
||
|
||
/**
|
||
* @var PasskeysAuthCustomerCookieRepository
|
||
*/
|
||
private PasskeysAuthCustomerCookieRepository $passkeysCustomerCookieRepository;
|
||
|
||
/**
|
||
* @var PasswordHasherFactoryInterface
|
||
*/
|
||
private PasswordHasherFactoryInterface $hashFactory;
|
||
|
||
/**
|
||
* constructor.
|
||
*
|
||
* @param EntityManagerInterface $entityManager
|
||
* @param EccubeConfig $eccubeConfig
|
||
* @param BaseInfoRepository $baseInfoRepository
|
||
* @param PasskeysAuthConfigRepository $PasskeysAuthConfigRepository
|
||
* @param PasskeysAuthCustomerCookieRepository $passkeysCustomerCookieRepository
|
||
* @param PasswordHasherFactoryInterface $hashFactory
|
||
*/
|
||
public function __construct(
|
||
EntityManagerInterface $entityManager,
|
||
EccubeConfig $eccubeConfig,
|
||
BaseInfoRepository $baseInfoRepository,
|
||
RequestStack $requestStack,
|
||
PasskeysAuthConfigRepository $PasskeysAuthConfigRepository,
|
||
PasskeysAuthCustomerCookieRepository $passkeysCustomerCookieRepository,
|
||
PasswordHasherFactoryInterface $hashFactory
|
||
) {
|
||
$this->entityManager = $entityManager;
|
||
$this->eccubeConfig = $eccubeConfig;
|
||
|
||
$this->baseInfo = $baseInfoRepository->find(1);
|
||
$this->request = $requestStack->getCurrentRequest();
|
||
$this->cookieName = $this->eccubeConfig->get('plugin_eccube_passkeys_customer_cookie_name');
|
||
$this->routeCookieName = $this->eccubeConfig->get('plugin_eccube_passkeys_route_customer_cookie_name');
|
||
|
||
$this->expire = (int) $this->eccubeConfig->get('plugin_eccube_passkeys_customer_expire');
|
||
$this->route_expire = (int) $this->eccubeConfig->get('plugin_eccube_passkeys_route_customer_expire');
|
||
|
||
$this->PasskeysAuthConfig = $PasskeysAuthConfigRepository->findOne();
|
||
$this->passkeysCustomerCookieRepository = $passkeysCustomerCookieRepository;
|
||
$this->hashFactory = $hashFactory;
|
||
}
|
||
|
||
/**
|
||
* @return array
|
||
*/
|
||
public function getDefaultAuthRoutes()
|
||
{
|
||
return $this->default_tfa_routes;
|
||
}
|
||
|
||
/**
|
||
* @required
|
||
*/
|
||
public function setContainer(ContainerInterface $container): ?ContainerInterface
|
||
{
|
||
$previous = $this->container;
|
||
$this->container = $container;
|
||
|
||
return $previous;
|
||
}
|
||
|
||
/**
|
||
* 2段階認証用Cookie生成.
|
||
*
|
||
* @param Customer $Customer
|
||
* @param null $route
|
||
*
|
||
* @return Cookie
|
||
*/
|
||
public function createAuthedCookie($Customer, $route = null): Cookie
|
||
{
|
||
$expire = $this->expire;
|
||
$cookieName = $this->cookieName;
|
||
if ($route != null) {
|
||
$includeRouts = $this->getIncludeRoutes();
|
||
if (in_array($route, $includeRouts) && $this->isAuthed($Customer, 'mypage')) {
|
||
$cookieName = $this->routeCookieName.'_'.$route;
|
||
$expire = $this->route_expire;
|
||
}
|
||
}
|
||
|
||
return $this->createRouteAuthCookie($Customer, $cookieName, $expire);
|
||
}
|
||
|
||
/**
|
||
* 要認証ルートを取得.
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getIncludeRoutes(): array
|
||
{
|
||
$routes = [];
|
||
$include = $this->PasskeysAuthConfig->getIncludeRoutes();
|
||
if ($include) {
|
||
$routes = preg_split('/\R/', $include);
|
||
}
|
||
|
||
return $routes;
|
||
}
|
||
|
||
/**
|
||
* 認証済みか?
|
||
*
|
||
* @param Customer $Customer
|
||
* @param null $route
|
||
*
|
||
* @return boolean
|
||
*/
|
||
public function isAuthed(Customer $Customer, $route = null): bool
|
||
{
|
||
$expire = $this->expire;
|
||
if ($route != null) {
|
||
$includeRouts = $this->getIncludeRoutes();
|
||
if (in_array($route, $includeRouts) && $this->isAuthed($Customer, 'mypage')) {
|
||
// 重要操作ルーティングの場合、
|
||
$cookieName = $this->routeCookieName.'_'.$route;
|
||
$expire = $this->route_expire;
|
||
} else {
|
||
// デフォルトルーティングの場合、
|
||
$cookieName = $this->cookieName;
|
||
}
|
||
|
||
return $this->isRouteAuthed($Customer, $cookieName, $expire);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* デフォルトルート・重要操作ルーティングは認証済みか
|
||
* データベースの中に保存しているデータとクッキー値を比較する
|
||
*
|
||
* @param Customer $Customer
|
||
* @param string $cookieName
|
||
* @param int $expire
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function isRouteAuthed(Customer $Customer, string $cookieName, int $expire): bool
|
||
{
|
||
if ($json = $this->request->cookies->get($cookieName)) {
|
||
$configs = json_decode($json);
|
||
|
||
/** @var PasskeysCustomerCookie[]|null $activeCookies */
|
||
$activeCookies = $this
|
||
->passkeysCustomerCookieRepository
|
||
->searchForCookie($Customer, $cookieName);
|
||
|
||
foreach ($activeCookies as $activeCookie) {
|
||
if (
|
||
$configs
|
||
&& isset($configs->{$Customer->getId()})
|
||
&& ($config = $configs->{$Customer->getId()})
|
||
&& property_exists($config, 'key')
|
||
&& $config->key === $activeCookie->getCookieValue()
|
||
&& (
|
||
$this->expire == 0
|
||
|| (property_exists($config, 'date') && ($config->date && $config->date > date('U', strtotime('-'.$expire))))
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 2段階認証用Cookie生成.
|
||
* クッキーデータをデータベースに保存する
|
||
*
|
||
* @param Customer $Customer
|
||
* @param string $cookieName
|
||
* @param int $expire
|
||
*
|
||
* @return mixed
|
||
*/
|
||
public function createRouteAuthCookie(Customer $Customer, string $cookieName, int $expire)
|
||
{
|
||
return $this->entityManager->wrapInTransaction(function (EntityManagerInterface $em) use ($expire, $cookieName, $Customer) {
|
||
$cookieData = $this->passkeysCustomerCookieRepository->generateCookieData(
|
||
$Customer,
|
||
$cookieName,
|
||
$expire,
|
||
$this->eccubeConfig->get('plugin_eccube_passkeys_route_cookie_value_character_length')
|
||
);
|
||
|
||
$configs = json_decode('{}');
|
||
if ($json = $this->request->cookies->get($cookieName)) {
|
||
$configs = json_decode($json);
|
||
}
|
||
|
||
$configs->{$Customer->getId()} = [
|
||
'key' => $cookieData->getCookieValue(),
|
||
'date' => time(),
|
||
];
|
||
|
||
$em->persist($cookieData);
|
||
$em->flush();
|
||
|
||
return new Cookie(
|
||
$cookieData->getCookieName(), // name
|
||
json_encode($configs), // value
|
||
$cookieData->getCookieExpireDate()->getTimestamp(), // expire
|
||
$this->request->getBasePath(), // path
|
||
null, // domain
|
||
$this->eccubeConfig->get('eccube_force_ssl') ? true : false, // secure
|
||
true, // httpOnly
|
||
false, // raw
|
||
$this->eccubeConfig->get('eccube_force_ssl') ? Cookie::SAMESITE_NONE : null // sameSite
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 二段階認証設定が有効か?
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function isEnabled(): bool
|
||
{
|
||
return $this->baseInfo->isPasskeysUse();
|
||
}
|
||
|
||
/**
|
||
* Passkey認証に関係しているクッキーだけを消す
|
||
*
|
||
* @param Request $request
|
||
* @param Response $response
|
||
*
|
||
* @return void
|
||
*/
|
||
public function clearPKAuthCookies(Request $request, Response $response)
|
||
{
|
||
foreach ($request->cookies->all() as $key => $cookie) {
|
||
if (
|
||
$this->str_contains($key, $this->cookieName) ||
|
||
$this->str_contains($key, $this->routeCookieName)
|
||
) {
|
||
// クッキーを消す
|
||
$response->headers->clearCookie($key);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Passkey sessionをチェックする
|
||
*
|
||
* @param String $session_id
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function checkSession($session_id, $rp)
|
||
{
|
||
//Post request to server using curl
|
||
$ch = curl_init();
|
||
|
||
curl_setopt($ch, CURLOPT_URL, 'https://fido2.amipro.me/usr/validsession');//'https://mac-air-m2.dqj-home.com/usr/validsession');
|
||
curl_setopt($ch, CURLOPT_POST, 1);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
|
||
//$json_body = '{"session": "'.$session_id.'", "rp": {"id": "'.$rp.'"}}';
|
||
|
||
$data = array(
|
||
'session'=>$session_id,
|
||
'rp'=> array(
|
||
'id'=>$rp
|
||
),
|
||
'debug_src'=>'checkSession'
|
||
);
|
||
$json_body = json_encode($data);
|
||
log_info('pk: checkSession json: ' . $json_body);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_body);//json_encode(['session' => $session_id, 'rp' => ['id' => $rp] ]));
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
|
||
log_info('pk: checkSession req: ' . $session_id . '|' . $rp);
|
||
|
||
$response = curl_exec($ch);
|
||
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
|
||
curl_close($ch);
|
||
|
||
log_info('pk: checkSession resp: ' . $status_code . '|' . $response);
|
||
$resp = json_decode($response, true);
|
||
return $resp && $resp['status'] && $resp['status'] === 'ok';
|
||
}
|
||
|
||
|
||
/***
|
||
* @param string $haystack
|
||
* @param string $needle
|
||
* @return bool
|
||
*
|
||
* @deprecated ECCUBEの最低PHPバージョンは8.0になったら, この関数を消してphp8.0からのstr_containsを利用する
|
||
*/
|
||
private function str_contains(string $haystack, string $needle)
|
||
{
|
||
return $needle !== '' && mb_strpos($haystack, $needle) !== false;
|
||
}
|
||
}
|