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