Files
Passkeys/Service/CustomerPasskeysAuthService.php
2025-10-06 21:45:33 +09:00

410 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
/**
* 段階認証用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;
}
}