first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<?php
namespace KupShop\UserOauthBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
class LanguagesSettingsOauthTab extends WindowTab
{
protected $title = 'settingsOauth';
protected $template = 'languagesSettingsOauth.tpl';
public static function getTypes()
{
return [
'languagesSettings' => 1,
];
}
}

View File

@@ -0,0 +1,5 @@
<?php
$txt_str['languagesSettingsOauthTab'] = [
'settingsOauth' => 'Přihlašování',
];

View File

@@ -0,0 +1,191 @@
<div id="settingsOauth" class="tab-pane fade in boxFlex">
<h1 class="h4 main-panel-title">Facebook</h1>
<div class="form-group">
<div class="col-md-2 control-label">Client ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_id', $body.data.oauth.facebook) && is_null($body.data.oauth.facebook.client_id)}
<input type="text" class="form-control input-sm" name="data[oauth][facebook][client_id]"
placeholder="{$dbcfg.oauth.facebook.client_id}"
value="{if $isEmpty} {else}{$body.data.oauth.facebook.client_id}{/if}">
{input_overwrite value_path=['oauth', 'facebook', 'client_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Client secret</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_secret', $body.data.oauth.facebook) && is_null($body.data.oauth.facebook.client_secret)}
<input type="text" class="form-control input-sm" name="data[oauth][facebook][client_secret]"
placeholder="{$dbcfg.oauth.facebook.client_secret}"
value="{if $isEmpty} {else}{$body.data.oauth.facebook.client_secret}{/if}">
{input_overwrite value_path=['oauth', 'facebook', 'client_secret'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Return URL</div>
<div class="col-md-7">
<span class="form-control input-sm disabled" disabled>{$cfg.Addr.full}login/check-facebook</span>
</div>
</div>
<h1 class="h4 main-panel-title">Google</h1>
<div class="form-group">
<div class="col-md-2 control-label">Client ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_id', $body.data.oauth.google) && is_null($body.data.oauth.google.client_id)}
<input type="text" class="form-control input-sm" name="data[oauth][google][client_id]"
placeholder="{$dbcfg.oauth.google.client_id}"
value="{if $isEmpty} {else}{$body.data.oauth.google.client_id}{/if}">
{input_overwrite value_path=['oauth', 'google', 'client_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Client secret</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_secret', $body.data.oauth.google) && is_null($body.data.oauth.google.client_secret)}
<input type="text" class="form-control input-sm" name="data[oauth][google][client_secret]"
placeholder="{$dbcfg.oauth.google.client_secret}"
value="{if $isEmpty} {else}{$body.data.oauth.google.client_secret}{/if}">
{input_overwrite value_path=['oauth', 'google', 'client_secret'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Return URL</div>
<div class="col-md-7">
<span class="form-control input-sm disabled" disabled>{$cfg.Addr.full}login/check-google</span>
</div>
</div>
{ifmodule USER_OAUTH__SEZNAM_LOGIN}
<h1 class="h4 main-panel-title">Seznam</h1>
<div class="form-group">
<div class="col-md-2 control-label">Client ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_id', $body.data.oauth.seznam) && is_null($body.data.oauth.seznam.client_id)}
<input type="text" class="form-control input-sm" name="data[oauth][seznam][client_id]"
placeholder="{$dbcfg.oauth.seznam.client_id}"
value="{if $isEmpty} {else}{$body.data.oauth.seznam.client_id}{/if}">
{input_overwrite value_path=['oauth', 'seznam', 'client_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Client secret</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_secret', $body.data.oauth.seznam) && is_null($body.data.oauth.seznam.client_secret)}
<input type="text" class="form-control input-sm" name="data[oauth][seznam][client_secret]"
placeholder="{$dbcfg.oauth.seznam.client_secret}"
value="{if $isEmpty} {else}{$body.data.oauth.seznam.client_secret}{/if}">
{input_overwrite value_path=['oauth', 'seznam', 'client_secret'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Return URL</div>
<div class="col-md-7">
<span class="form-control input-sm disabled" disabled>{$cfg.Addr.full}login/check-seznam</span>
</div>
</div>
{/ifmodule}
{ifmodule USER_OAUTH__AMAZON_LOGIN}
<h1 class="h4 main-panel-title">Amazon</h1>
<div class="form-group">
<div class="col-md-2 control-label">Client ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_id', $body.data.oauth.amazon) && is_null($body.data.oauth.amazon.client_id)}
<input type="text" class="form-control input-sm" name="data[oauth][amazon][client_id]"
placeholder="{$dbcfg.oauth.amazon.client_id}"
value="{if $isEmpty} {else}{$body.data.oauth.amazon.client_id}{/if}">
{input_overwrite value_path=['oauth', 'amazon', 'client_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Client secret</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_secret', $body.data.oauth.amazon) && is_null($body.data.oauth.amazon.client_secret)}
<input type="text" class="form-control input-sm" name="data[oauth][amazon][client_secret]"
placeholder="{$dbcfg.oauth.amazon.client_secret}"
value="{if $isEmpty} {else}{$body.data.oauth.amazon.client_secret}{/if}">
{input_overwrite value_path=['oauth', 'amazon', 'client_secret'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Return URL</div>
<div class="col-md-7">
<span class="form-control input-sm disabled" disabled>{$cfg.Addr.full}login/check-amazon</span>
</div>
</div>
{/ifmodule}
{ifmodule USER_OAUTH__APPLE_LOGIN}
<h1 class="h4 main-panel-title">Apple</h1>
<div class="form-group">
<div class="col-md-2 control-label">Client ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('client_id', $body.data.oauth.apple) && is_null($body.data.oauth.apple.client_id)}
<input type="text" class="form-control input-sm" name="data[oauth][apple][client_id]"
placeholder="{$dbcfg.oauth.apple.client_id}"
value="{if $isEmpty} {else}{$body.data.oauth.apple.client_id}{/if}">
{input_overwrite value_path=['oauth', 'apple', 'client_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Team ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('team_id', $body.data.oauth.apple) && is_null($body.data.oauth.apple.team_id)}
<input type="text" class="form-control input-sm" name="data[oauth][apple][team_id]"
placeholder="{$dbcfg.oauth.apple.team_id}"
value="{if $isEmpty} {else}{$body.data.oauth.apple.team_id}{/if}">
{input_overwrite value_path=['oauth', 'apple', 'team_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Key ID</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('key_id', $body.data.oauth.apple) && is_null($body.data.oauth.apple.key_id)}
<input type="text" class="form-control input-sm" name="data[oauth][apple][key_id]"
placeholder="{$dbcfg.oauth.apple.key_id}"
value="{if $isEmpty} {else}{$body.data.oauth.apple.key_id}{/if}">
{input_overwrite value_path=['oauth', 'apple', 'key_id'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Auth key</div>
<div class="col-md-7">
<div class="input-group" data-input-overwrite>
{$isEmpty = array_key_exists('auth_key', $body.data.oauth.apple) && is_null($body.data.oauth.apple.auth_key)}
<input type="text" class="form-control input-sm" name="data[oauth][apple][auth_key]"
placeholder="{$dbcfg.oauth.apple.auth_key}"
value="{if $isEmpty} {else}{$body.data.oauth.apple.auth_key}{/if}">
<div class="input-group-btn">
{insert_file_browse link="auth_key"}
</div>
{input_overwrite value_path=['oauth', 'apple', 'auth_key'] isEmpty=$isEmpty}
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">Return URL</div>
<div class="col-md-7">
<span class="form-control input-sm disabled" disabled>{$cfg.Addr.full}login/check-apple</span>
</div>
</div>
{/ifmodule}
</div>

View File

@@ -0,0 +1,95 @@
<?php
namespace KupShop\UserOauthBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class BindExternalAccountController extends AbstractController
{
use \DatabaseCommunication;
/** @var SessionInterface */
private $session;
/** @var TokenStorageInterface */
private $tokenStorage;
public function __construct(SessionInterface $session, TokenStorageInterface $tokenStorage)
{
$this->session = $session;
$this->tokenStorage = $tokenStorage;
}
/**
* @Route("/login-bind/{provider}/")
*/
public function bindAction(Request $request)
{
$provider = $request->get('provider');
$this->session->set('bind_external_account', $provider);
$next = $request->query->get('next');
if (empty($next)) {
$next = $request->headers->get('referer');
}
if (!empty($next)) {
if (parse_url($next, PHP_URL_QUERY)) {
$next .= '&wpj_utm=auth';
} else {
$next .= '?wpj_utm=auth';
}
} else {
$next = path('account', ['wpj_utm' => 'auth']);
}
return new RedirectResponse(path('hwi_oauth_service_redirect', [
'service' => $provider,
'next' => $next,
]));
}
/**
* @Route("/login-unbind/{id}/")
*/
public function unbindAction(Request $request)
{
$token = $this->tokenStorage->getToken();
if ($user = $token?->getUser()) {
if (method_exists($user, 'getID')) {
$userProvider = $this->selectSQL('users_provider_ids', [
'id' => $request->get('id'),
'id_user' => $user->getID(),
])->fetch();
$result = $this->deleteSQL('users_provider_ids', [
'id' => $request->get('id'),
'id_user' => $user->getID(),
]);
if ($result == 1) {
if ($request->isXmlHttpRequest()) {
return new Response();
} else {
addUserMessage(
sprintf(
translate('oauth', 'login')['binding_removed'],
ucfirst($userProvider['provider']),
$userProvider['email']
),
'success'
);
}
}
}
}
if ($request->isXmlHttpRequest()) {
return new Response('', 500);
} else {
return new RedirectResponse(path('settings'));
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace KupShop\UserOauthBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
class CartRegisterController extends AbstractController
{
/** @var SessionInterface */
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
/**
* @Route("/login-cart/{provider}/")
*/
public function loginCartAction(Request $request)
{
$provider = $request->get('provider');
$previous = $request->server->get('HTTP_REFERER');
if ($previous) {
$this->session->set('oauth_register_redirect', $previous);
}
return new RedirectResponse("/connect/{$provider}");
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ResourceOwnerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $key => $definition) {
if (str_starts_with($key, 'hwi_oauth.resource_owner.')) {
$definition->setAutowired(true);
}
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => [
['onLogout', 200],
],
];
}
public function onLogout(LogoutEvent $event): void
{
// set `logout: true` session to notify js-shop
$event->getRequest()->getSession()
->set('logout', true);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Helper\NonceGenerator;
use KupShop\AgeVerifyBundle\Utils\AgeVerifyUtil;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Util\Contexts;
use Query\Operator;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\Service\Attribute\Required;
class BankIdResourceOwner extends GenericOAuth2ResourceOwner implements ICustomResourceOwner
{
public const SANDBOX_URL = 'https://oidc.sandbox.bankid.cz/';
public const PROD_URL = 'https://oidc.bankid.cz/';
private ?AgeVerifyUtil $ageVerifyUtil = null;
public function __construct(
$httpClient,
HttpUtils $httpUtils,
array $options,
$name,
RequestDataStorageInterface $storage,
) {
if (!isLocalDevelopment()) {
$options['client_id'] = $this->getClientId();
$options['client_secret'] = $this->getClientSecret();
}
$options['access_token_url'] = $this->getUrl('token');
$options['authorization_url'] = $this->getUrl('auth');
$options['infos_url'] = $this->getUrl('userinfo');
$options['csrf'] = true;
parent::__construct($httpClient, $httpUtils, $options, $name, $storage);
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
$data = [];
if ($birthdate = $response->getBirthdate()) {
$data['birthdate'] = $birthdate;
}
if ($name = $response->getFirstName()) {
$data['name'] = $name;
}
if ($surname = $response->getLastName()) {
$data['surname'] = $surname;
}
if (!empty($data)) {
sqlQueryBuilder()->update('users')
->directValues($data)
->where(Operator::equals(['id' => $user->id]))
->execute();
}
$date = \DateTime::createFromFormat('Y-m-d', $birthdate)->add(\DateInterval::createFromDateString('+18YEARS'));
$this->ageVerifyUtil?->setVerificationData(legalAge: $date <= (new \DateTime()) ? 'Y' : 'N', type: 'bankId', userId: $user->id);
}
protected function getClientId()
{
if (isLocalDevelopment()) {
return $this->options['client_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['bank_id']['client_id'] ?? '';
}
protected function getClientSecret()
{
if (isLocalDevelopment()) {
return $this->options['client_secret'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['bank_id']['client_secret'] ?? '';
}
protected function getUrl($path)
{
if (isDevelopment() || \Settings::getDefault()['oauth']['bank_id']['sandbox']) {
return self::SANDBOX_URL.$path;
}
return self::PROD_URL.$path;
}
protected function getRedirectUri()
{
if (isDevelopment()) {
return Config::get()['Addr']['full_original'].'login/check-bankid';
}
/** @var DomainContext $domainContext */
$domainContext = Contexts::get(DomainContext::class);
return $domainContext->getActiveWithScheme().'/login/check-bankid';
}
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$extraParameters['redirect_uri'] = $this->getRedirectUri();
$extraParameters['nonce'] = NonceGenerator::generate();
return parent::getAuthorizationUrl($redirectUri, $extraParameters);
}
protected function doGetTokenRequest($url, array $parameters = [])
{
$parameters['client_id'] = $this->getClientId();
$parameters['client_secret'] = $this->getClientSecret();
$parameters['redirect_uri'] = $this->getRedirectUri();
return $this->httpRequest($url, http_build_query($parameters, '', '&'));
}
#[Required]
public function setAgeVerifyUtil(?AgeVerifyUtil $ageVerifyUtil): void
{
$this->ageVerifyUtil = $ageVerifyUtil;
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\AppleResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class CustomAppleResourceOwner extends CustomResourceOwner
{
public function __construct(
HttpClientInterface $httpClient,
HttpUtils $httpUtils,
array $options,
string $name,
RequestDataStorageInterface $storage,
) {
$options['client_id'] = $this->getClientId($options);
$options['team_id'] = $this->getTeamId($options);
$options['key_id'] = $this->getKeyId($options);
$options['auth_key'] = $this->getAuthKey($options);
parent::__construct($httpClient, $httpUtils, $options, $name, $storage);
}
public function createResourceOwner($httpClient, $httpUtils, $options, $name, $storage)
{
return new AppleResourceOwner($httpClient, $httpUtils, $options, $name, $storage);
}
protected function getClientId(array $options): string
{
if (isLocalDevelopment()) {
return $options['client_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['apple']['client_id'] ?? '';
}
protected function getTeamId(array $options): string
{
if (isLocalDevelopment()) {
return $options['options']['team_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['apple']['team_id'] ?? '';
}
protected function getKeyId(array $options): string
{
if (isLocalDevelopment()) {
return $options['options']['key_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['apple']['key_id'] ?? '';
}
protected function getAuthKey(array $options): string
{
if (isLocalDevelopment()) {
return $options['options']['auth_key'];
}
$settings = \Settings::getDefault();
$path = $settings['oauth']['apple']['auth_key'] ?? '';
return file_get_contents(substr($path, 1)) ?? '';
}
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$extraParameters['skip_tolocalhost'] = true;
return parent::getAuthorizationUrl($redirectUri, $extraParameters);
}
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = [])
{
$extraParameters['skip_tolocalhost'] = true;
return parent::getAccessToken($request, $redirectUri, $extraParameters);
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
// TODO: Implement updateUserData() method.
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\FacebookResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
class CustomFacebookResourceOwner extends CustomResourceOwner
{
public function createResourceOwner($httpClient, $httpUtils, $options, $name, $storage)
{
return new FacebookResourceOwner($httpClient, $httpUtils, $options, $name, $storage);
}
/**
* Get the url to the profile picture.
*
* @return string|null
*/
protected function getProfilePicture(?string $user_name = null)
{
if ($user_name) {
return 'https://graph.facebook.com/'.$user_name.'/picture?type=square';
}
return null;
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
$profilePicture = $this->getProfilePicture($response->getUsername());
if ($profilePicture) {
$user->setCustomData('profilePicture', $profilePicture);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GoogleResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
class CustomGoogleResourceOwner extends CustomResourceOwner
{
public function createResourceOwner($httpClient, $httpUtils, $options, $name, $storage)
{
return new GoogleResourceOwner($httpClient, $httpUtils, $options, $name, $storage);
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
$profilePicture = $response->getProfilePicture();
if ($profilePicture) {
$user->setCustomData('profilePicture', $profilePicture);
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\OAuth\StateInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\Security\Http\HttpUtils;
abstract class CustomResourceOwner implements ICustomResourceOwner, ResourceOwnerInterface
{
protected ResourceOwnerInterface $resourceOwner;
public function __construct(
$httpClient,
HttpUtils $httpUtils,
array $options,
$name,
RequestDataStorageInterface $storage,
) {
if (!isLocalDevelopment()) {
$settings = \Settings::getDefault();
if (!empty($settings['oauth'][$name]['client_id'])) {
$options['client_id'] = $settings['oauth'][$name]['client_id'];
}
if (!empty($settings['oauth'][$name]['client_secret'])) {
$options['client_secret'] = $settings['oauth'][$name]['client_secret'];
}
}
$this->resourceOwner = $this->createResourceOwner($httpClient, $httpUtils, $options, $name, $storage);
}
abstract public function createResourceOwner($httpClient, $httpUtils, $options, $name, $storage);
public function getUserInformation(array $accessToken, array $extraParameters = [])
{
return $this->resourceOwner->getUserInformation($accessToken, $extraParameters);
}
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
if (isLocalDevelopment() && !($extraParameters['skip_tolocalhost'] ?? false)) {
// Na locale využijeme tolocalhost.com služby, aby nám poskytla veřejnou doménu a pak přesměovala na kupshop.local
$redirectUri = str_replace('www.kupshop.local', 'tolocalhost.com', $redirectUri);
}
return $this->resourceOwner->getAuthorizationUrl($redirectUri, $extraParameters);
}
public function getAccessToken(HttpRequest $request, $redirectUri, array $extraParameters = [])
{
if (isLocalDevelopment() && !($extraParameters['skip_tolocalhost'] ?? false)) {
// Na locale využijeme tolocalhost.com služby, aby nám poskytla veřejnou doménu a pak přesměovala na kupshop.local
$redirectUri = str_replace('www.kupshop.local', 'tolocalhost.com', $redirectUri);
}
return $this->resourceOwner->getAccessToken($request, $redirectUri, $extraParameters);
}
public function isCsrfTokenValid($csrfToken)
{
return $this->resourceOwner->isCsrfTokenValid($csrfToken);
}
public function getName(?UserResponseInterface $response = null)
{
return $this->resourceOwner->getName();
}
public function getOption($name)
{
return $this->resourceOwner->getOption($name);
}
public function handles(HttpRequest $request)
{
return $this->resourceOwner->handles($request);
}
public function setName($name)
{
$this->resourceOwner->setName($name);
}
public function addPaths(array $paths)
{
$this->resourceOwner->addPaths($paths);
}
public function refreshAccessToken($refreshToken, array $extraParameters = [])
{
$this->resourceOwner->refreshAccessToken($refreshToken, $extraParameters);
}
public function getState(): StateInterface
{
return $this->resourceOwner->getState();
}
public function storeState(?StateInterface $state = null)
{
$this->resourceOwner->storeState($state);
}
public function addStateParameter(string $key, string $value): void
{
$this->resourceOwner->addStateParameter($key, $value);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
interface ICustomResourceOwner
{
public function updateUserData(\User $user, UserResponseInterface $response);
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use KupShop\AgeVerifyBundle\Utils\AgeVerifyUtil;
use Query\Operator;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Service\Attribute\Required;
class MojeIDResourceOwner extends GenericOAuth2ResourceOwner implements ICustomResourceOwner
{
// pro testování lze pouzit testovaci ucet https://mojeid.regtest.nic.cz/index.html
public const SANDBOX_URL = 'https://mojeid.regtest.nic.cz/oidc/';
public const PROD_URL = 'https://mojeid.cz/oidc/';
private ?AgeVerifyUtil $ageVerifyUtil = null;
public function __construct(
HttpClientInterface $httpClient,
HttpUtils $httpUtils,
array $options,
string $name,
RequestDataStorageInterface $storage,
) {
if (!isLocalDevelopment()) {
$options['client_id'] = $this->getClientId();
$options['client_secret'] = $this->getClientSecret();
}
$options['access_token_url'] = $this->getUrl('token/');
$options['authorization_url'] = $this->getUrl('authorization/');
$options['infos_url'] = $this->getUrl('userinfo/');
parent::__construct($httpClient, $httpUtils, $options, $name, $storage);
}
protected function getUrl($path): string
{
if (isDevelopment()) {
return self::SANDBOX_URL.$path;
}
return self::PROD_URL.$path;
}
protected function getClientId()
{
if (isLocalDevelopment()) {
return $this->options['client_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['mojeid']['client_id'] ?? '';
}
protected function getClientSecret()
{
if (isLocalDevelopment()) {
return $this->options['client_secret'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['mojeid']['client_secret'] ?? '';
}
protected function doGetTokenRequest($url, array $parameters = [])
{
$parameters['client_id'] = $this->getClientId();
$parameters['client_secret'] = $this->getClientSecret();
return $this->httpRequest($url, http_build_query($parameters, '', '&'));
}
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
return parent::getAuthorizationUrl($redirectUri, [
'claims' => json_encode([
'id_token' => [
'birthdate' => [
'essential' => true,
],
'name' => [
'essential' => true,
],
'given_name' => [
'essential' => true,
],
'family_name' => [
'essential' => true,
],
'email' => [
'essential' => true,
],
'address' => [
'essential' => false,
],
'mojeid_valid' => ['essential' => true],
],
]),
]);
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
$data = [];
if ($birthdate = $response->getBirthdate()) {
$data['birthdate'] = $birthdate;
}
if ($name = $response->getFirstName()) {
$data['name'] = $name;
}
if ($surname = $response->getLastName()) {
$data['surname'] = $surname;
}
if ($address = $response->getAddress()) {
$data['street'] = $address['street_address'] ?? '';
$data['city'] = $address['locality'] ?? '';
$data['zip'] = $address['postal_code'] ?? '';
$data['country'] = $address['country'] ?? '';
}
if (!empty($data)) {
sqlQueryBuilder()->update('users')
->directValues($data)
->where(Operator::equals(['id' => $user->id]))
->execute();
}
if ($birthdate) {
$date = \DateTime::createFromFormat('Y-m-d', $birthdate)->add(\DateInterval::createFromDateString('+18YEARS'));
$this->ageVerifyUtil?->setVerificationData(legalAge: $date <= (new \DateTime()) ? 'Y' : 'N', type: 'mojeid', userId: $user->id);
}
}
#[Required]
public function setAgeVerifyUtil(?AgeVerifyUtil $ageVerifyUtil): void
{
$this->ageVerifyUtil = $ageVerifyUtil;
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\RequestDataStorageInterface;
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\GenericOAuth2ResourceOwner;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Util\Contexts;
use Query\Operator;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SeznamResourceOwner extends GenericOAuth2ResourceOwner implements ICustomResourceOwner
{
public const URL = 'https://login.szn.cz/api/v1/';
public function __construct(
HttpClientInterface $httpClient,
HttpUtils $httpUtils,
array $options,
string $name,
RequestDataStorageInterface $storage,
) {
if (!isLocalDevelopment()) {
$options['client_id'] = $this->getClientId();
$options['client_secret'] = $this->getClientSecret();
}
$options['access_token_url'] = $this->getUrl('oauth/token');
$options['authorization_url'] = $this->getUrl('oauth/auth');
$options['infos_url'] = $this->getUrl('user');
parent::__construct($httpClient, $httpUtils, $options, $name, $storage);
}
public function updateUserData(\User $user, UserResponseInterface $response)
{
$data = [];
if ($firstName = $response->getData()['firstname']) {
$data['name'] = $firstName;
}
if ($lastName = $response->getData()['lastname']) {
$data['surname'] = $lastName;
}
if (!empty($data)) {
sqlQueryBuilder()->update('users')
->directValues($data)
->where(Operator::equals(['id' => $user->id]))
->execute();
}
}
protected function getClientId(): string
{
if (isLocalDevelopment()) {
return $this->options['client_id'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['seznam']['client_id'] ?? '';
}
protected function getClientSecret(): string
{
if (isLocalDevelopment()) {
return $this->options['client_secret'];
}
$settings = \Settings::getDefault();
return $settings['oauth']['seznam']['client_secret'] ?? '';
}
protected function getUrl(string $path): string
{
return self::URL.$path;
}
protected function getRedirectUri(): string
{
if (isDevelopment()) {
return Config::get()['Addr']['full'].'login/check-seznam/';
}
/** @var DomainContext $domainContext */
$domainContext = Contexts::get(DomainContext::class);
return $domainContext->getActiveWithScheme().'/login/check-seznam/';
}
public function getAuthorizationUrl($redirectUri, array $extraParameters = [])
{
$extraParameters['redirect_uri'] = $this->getRedirectUri();
return parent::getAuthorizationUrl($redirectUri, $extraParameters);
}
protected function doGetTokenRequest($url, array $parameters = [])
{
$parameters['client_id'] = $this->getClientId();
$parameters['client_secret'] = $this->getClientSecret();
$parameters['redirect_uri'] = $this->getRedirectUri();
return $this->httpRequest($url, http_build_query($parameters, '', '&'));
}
}

View File

@@ -0,0 +1,3 @@
imports:
- { resource: 'security.yml' }
- { resource: 'hwiOauth.yml' }

View File

@@ -0,0 +1,113 @@
parameters:
facebook_client_id: 947379488985980
facebook_client_secret: b3fb9b6cd21e52ea79e19a5a18179410
google_client_id: '787722845021-vspi8iechcta8c67esethe2oue9qifdd.apps.googleusercontent.com'
google_client_secret: 'lzMuLFodGN1dk3zkgY0v1E_t'
bankid_client_id: '8bac41a4-e6d5-455b-bea1-83ea0fdc65a5'
bankid_client_secret: 'ANWPPe1MivCVHtqFPlBZhVZNSGmH0uYKoKwfLD1uVONpYd86us4jSu23EbzabTEwxFZ-soqzhvuyN_s5TYX0zxM'
mojeid_client_id: 'UG315IdkvnQ6'
mojeid_client_secret: '6f5a8ae93b96050ab7a6a6dc8d1222ea1de5e5cde06d6fde0d208e73'
seznam_client_id: '373b52f07568beba6122c1ab386f06340befe6076be51d30'
seznam_client_secret: '046f01b7c6ed1cf39ae7b6dded8189afd593e3196fdd1b67'
amazon_client_id: 'amzn1.application-oa2-client.cb5d9d73b8c4464fa53a2aeaa5684e3f'
amazon_client_secret: 'amzn1.oa2-cs.v1.307eccb59d9a78627985e051f335cf37290b936282d8f4f2a01bb537ef2a6190'
apple_client_id: 'local.kupshop'
env(APPLE_AUTH_KEY_PATH): "%kernel.project_dir%/AuthKey_PY9XTX36J3.p8"
hwi_oauth.resource_owner.facebook.class: 'KupShop\UserOauthBundle\ResourceOwner\CustomFacebookResourceOwner'
hwi_oauth.resource_owner.google.class: 'KupShop\UserOauthBundle\ResourceOwner\CustomGoogleResourceOwner'
hwi_oauth.resource_owner.bankid.class: 'KupShop\UserOauthBundle\ResourceOwner\BankIdResourceOwner'
hwi_oauth.resource_owner.mojeid.class: 'KupShop\UserOauthBundle\ResourceOwner\MojeIDResourceOwner'
hwi_oauth.resource_owner.seznam.class: 'KupShop\UserOauthBundle\ResourceOwner\SeznamResourceOwner'
hwi_oauth.resource_owner.apple.class: 'KupShop\UserOauthBundle\ResourceOwner\CustomAppleResourceOwner'
hwi_oauth:
# list of names of the firewalls in which this bundle is active, this setting MUST be set
firewall_names: [main]
target_path_parameter: next
resource_owners:
facebook:
type: facebook
client_id: '%facebook_client_id%'
client_secret: '%facebook_client_secret%'
scope: "email"
infos_url: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email,picture.type(square)"
paths:
email: email
profilepicture: picture.data.url
options:
auth_type: rerequest # Re-asking for Declined Permissions
google:
type: google
client_id: '%google_client_id%'
client_secret: '%google_client_secret%'
scope: 'email profile'
paths:
email: email
profilepicture: picture
bankid:
type: oauth2
client_id: '%bankid_client_id%'
client_secret: '%bankid_client_secret%'
class: KupShop\UserOauthBundle\ResourceOwner\BankIdResourceOwner
scope: "openid profile.birthdate profile.email profile.name profile.phonenumber"
user_response_class: KupShop\UserOauthBundle\Response\BankIdPathUserResponse
paths:
identifier: sub
realname: name
email: email
phone: phonenumber
birthdate: birthdate
firstname: given_name
lastname: family_name
mojeid:
type: oauth2
client_id: '%mojeid_client_id%'
client_secret: '%mojeid_client_secret%'
class: KupShop\UserOauthBundle\ResourceOwner\MojeIDResourceOwner
scope: 'openid profile birthdate email address'
user_response_class: KupShop\UserOauthBundle\Response\MojeIDPathUserResponse
paths:
identifier: sub
realname: name
email: email
firstname: given_name
lastname: family_name
birthdate: birthdate
address: address
seznam:
type: oauth2
client_id: '%seznam_client_id%'
client_secret: '%seznam_client_secret%'
class: KupShop\UserOauthBundle\ResourceOwner\SeznamResourceOwner
scope: "openid, email, identity"
paths:
identifier: oauth_user_id
email: email
firstname: firstname
lastname: lastname
amazon:
type: oauth2
client_id: '%amazon_client_id%'
client_secret: '%amazon_client_secret%'
class: HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\AmazonResourceOwner
scope: "profile"
user_response_class: KupShop\UserOauthBundle\Response\AmazonPathUserResponse
paths:
identifier: user_id
email: email
apple:
type: apple
client_id: '%apple_client_id%'
client_secret: 'auto'
scope: "name email"
class: KupShop\UserOauthBundle\ResourceOwner\CustomAppleResourceOwner
options:
auth_key: |
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgMG1ueHW5U4HeHgHX
xDQiegpjNOkHJt5oil0CFujR/6CgCgYIKoZIzj0DAQehRANCAAR0l8D5/hsL5kHH
Q0FMIyd+Yuvx4R18ykyoSJ1r/FuM8Jn8aQ3NPMpaBcMTTbIjkrhtAHMHDI8r3ZYm
7eEe7yw+
-----END PRIVATE KEY-----
key_id: 'PY9XTX36J3'
team_id: '3GJ63RH8PG'

View File

@@ -0,0 +1,26 @@
hwi_oauth_redirect:
resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /connect
facebook_check:
path: /login/check-facebook
google_check:
path: /login/check-google
bankid_check:
path: /login/check-bankid
mojeid_check:
path: /login/check-mojeid
seznam_check:
path: /login/check-seznam
apple_check:
path: /login/check-apple
content:
resource: "@UserOauthBundle/Controller/"
type: annotation

View File

@@ -0,0 +1,24 @@
security:
providers:
kupshop_useroauthbundle:
id: KupShop\UserOauthBundle\Security\OauthUserProvider
firewalls:
admin:
pattern: ^/(admin|_z)/
main:
oauth:
provider: kupshop_useroauthbundle
resource_owners:
facebook: "/login/check-facebook"
google: "/login/check-google"
bankid: "/login/check-bankid"
mojeid: "/login/check-mojeid"
seznam: "/login/check-seznam"
amazon: "/login/check-amazon"
apple: "/login/check-apple"
login_path: /
failure_path: kupshop_user_login_login
oauth_user_provider:
service: KupShop\UserOauthBundle\Security\OauthUserProvider
access_denied_handler: KupShop\UserOauthBundle\Security\AccessDeniedHandler

View File

@@ -0,0 +1,12 @@
services:
_defaults:
autowire: true
autoconfigure: true
KupShop\UserOauthBundle\:
resource: ../../{Admin/Tabs,EventSubscriber}
KupShop\UserOauthBundle\Security\OauthUserProvider: ~
KupShop\UserOauthBundle\Security\AccessDeniedHandler: ~
KupShop\UserOauthBundle\Controller\BindExternalAccountController: ~
KupShop\UserOauthBundle\Controller\CartRegisterController: ~

View File

@@ -0,0 +1,25 @@
<?php
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
function smarty_function_get_logged_user_provider_ids($params, &$smarty)
{
/** @var $tokenStorage TokenStorage */
$tokenStorage = ServiceContainer::getService('security.token_storage');
$token = $tokenStorage->getToken();
if ($token && ($user = $token->getUser())) {
if (method_exists($user, 'getID')) {
$providers = sqlQueryBuilder()->select('*')->from('users_provider_ids')
->where('id_user=:id_user')->setParameter('id_user', $user->getID())
->orderBy('provider')->addOrderBy('email')->execute()->fetchAll();
if (!empty($params['assign'])) {
$smarty->assign($params['assign'], $providers);
} else {
return $providers;
}
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace KupShop\UserOauthBundle\Resources\upgrade;
class UserOauthUpgrade extends \UpgradeNew
{
public function check_usersProviderIDs()
{
return $this->checkTableExists('users_provider_ids');
}
/** Add table users_provider_ids */
public function upgrade_usersProviderIDs()
{
sqlQuery('CREATE TABLE IF NOT EXISTS users_provider_ids
(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
id_user INT(11) UNSIGNED NOT NULL,
provider VARCHAR(255) NOT NULL,
client_id VARCHAR(255) NOT NULL,
email VARCHAR(255) DEFAULT NULL,
FOREIGN KEY (id_user)
REFERENCES users(id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE INDEX users_provider_ids_provider_index ON users_provider_ids (provider);
CREATE UNIQUE INDEX users_provider_ids_provider_client_id_uindex
ON users_provider_ids (provider, client_id);
');
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\Response;
use HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse;
class AmazonPathUserResponse extends PathUserResponse
{
public function getFirstName()
{
return explode(' ', $this->getData()['name'])[0] ?? '';
}
public function getLastName()
{
$lastName = explode(' ', $this->getData()['name']);
return end($lastName) ?? '';
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\Response;
use HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse;
class BankIdPathUserResponse extends PathUserResponse
{
public function getBirthdate()
{
return $this->getValueForPath('birthdate');
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace KupShop\UserOauthBundle\Response;
use HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse;
class MojeIDPathUserResponse extends PathUserResponse
{
public function getBirthdate()
{
return $this->getValueForPath('birthdate');
}
public function getAddress()
{
return $this->getValueForPath('address');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response
{
if ($accessDeniedException instanceof OauthBindFailedException) {
addUserMessage(translate('oauth', 'login')['bind_failed']);
return new RedirectResponse(path('settings'));
} elseif ($accessDeniedException instanceof OauthBindRefusedException) {
addUserMessage(translate('oauth', 'login')['bind_refused']);
return new RedirectResponse(path('settings'));
} elseif ($accessDeniedException instanceof OauthBindSuccessException) {
return new RedirectResponse(path('settings', ['wpj_utm' => 'auth']));
} elseif ($accessDeniedException instanceof ShouldLogOutException) {
addUserMessage(translate('oauth', 'login')['should_logout']);
return new RedirectResponse(path('account'));
}
return null;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
/**
* Throw this exception if clientID is not found but email is known.
*/
class ClientIDNotFoundException extends UserNotFoundException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class OauthBindFailedException extends AccessDeniedException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class OauthBindRefusedException extends AccessDeniedException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class OauthBindSuccessException extends AccessDeniedException
{
}

View File

@@ -0,0 +1,44 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use KupShop\UserBundle\Security\User;
class OauthUser extends User
{
protected $provider;
protected $clientID;
public function __construct(
int $id,
string $email,
?string $provider = null,
?string $clientID = null,
array $roles,
$kupshopUser,
) {
parent::__construct($id, $email, null, $roles, $kupshopUser);
$this->id = $id;
$this->email = $email;
$this->provider = $provider;
$this->clientID = $clientID;
$this->roles = $roles;
$this->kupshopUser = $kupshopUser;
}
public function getPassword(): string
{
return '';
}
public function getProvider()
{
return $this->provider;
}
public function getClientID()
{
return $this->clientID;
}
}

View File

@@ -0,0 +1,312 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
use KupShop\GraphQLBundle\EventListener\JsShopRefreshListener;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\UserBundle\Event\UserRegisteredEvent;
use KupShop\UserOauthBundle\ResourceOwner\ICustomResourceOwner;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class OauthUserProvider implements UserProviderInterface, OAuthAwareUserProviderInterface
{
use \DatabaseCommunication;
/** @var EventDispatcherInterface */
protected $eventDispatcher;
public function __construct(
protected TokenStorageInterface $tokenStorage,
protected SessionInterface $session,
protected RequestStack $requestStack,
) {
}
public function loadUserByUsername($username)
{
return $this->loadUserByIdentifier($username);
}
public function loadUserByIdentifier(string $identifier): UserInterface
{
throw new UserNotFoundException();
}
/**
* Refreshes the user for the account interface.
*
* It is up to the implementation to decide if the user data should be
* totally reloaded (e.g. from the database), or if the UserInterface
* object can just be merged into some internal array of users / identity
* map.
*
* @throws UnsupportedUserException if the account is not supported
*/
public function refreshUser(UserInterface $user): UserInterface
{
if ($user instanceof OauthUser) {
$userObject = $this->getUserByClientID($user->getProvider(), $user->getClientID());
if ($userObject) {
$userObject->activateUser();
return new OauthUser(
$userObject->id,
$userObject->email,
$user->getProvider(),
$user->getClientID(),
['ROLE_USER'],
$userObject
);
} else {
// this usually happens when user unbinds his fb/google account and is currently logged in with it
$this->tokenStorage->setToken(null);
// hack to preserve userMessages
$userMessages = $this->session->get('userMessages');
$this->session->invalidate();
$this->session->set('userMessages', $userMessages);
if (findModule(\Modules::JS_SHOP)) {
// set `jsShopReload: true` session to notify js-cart
$this->session->set(JsShopRefreshListener::SESSION_NAME, true);
}
addUserMessage(translate('oauth', 'login')['invalid_login']);
redirection(path('kupshop_user_login_login'));
}
}
throw new UnsupportedUserException();
}
/**
* Whether this provider supports the given user class.
*
* @param string $class
*/
public function supportsClass($class): bool
{
return $class === OauthUser::class;
}
private function getUserByClientID(string $provider, string $clientID)
{
$providerRow = $this->selectSQL('users_provider_ids', [
'provider' => $provider,
'client_id' => $clientID,
])->fetch();
if ($providerRow) {
$user = \User::createFromId($providerRow['id_user']);
}
return $user ?? false;
}
/**
* Loads the user by a given UserResponseInterface object.
*
* @return UserInterface
*
* @throws UserNotFoundException if the user is not found
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$email = $response->getEmail();
$resourceOwner = $response->getResourceOwner();
$provider = $resourceOwner->getName();
$clientID = $response->getUsername();
$user = $this->getUserByClientID($provider, $clientID ?? '');
$token = $this->tokenStorage->getToken();
if ($user && !$user->isActive()) {
throw new UserNotFoundException();
}
if ($user && $token?->getUser()) {
if ($this->session->get('bind_external_account') === $provider) {
// external account is already linked so refuse bind
$this->session->remove('bind_external_account');
throw new OauthBindRefusedException('Account already binded.');
} else {
// unintended bind (eg: when user is already logged in, but goes to /connect/google without proper session)
throw new ShouldLogOutException('Unintended bind.');
}
}
// bind account to current user
// to restrict binding for non logged users add condition: $token && $token->isAuthenticated()
if (!$user) {
$tokenUsername = $token?->getUser() ? $token->getUserIdentifier() : null;
// unintended bind check
if ($tokenUsername && $this->session->get('bind_external_account') !== $provider) {
throw new OauthBindRefusedException('Unintended bind.');
}
$this->session->remove('bind_external_account');
$user = \User::createFromLogin($tokenUsername ?? $email);
if ($user) {
try {
$user = sqlGetConnection()->transactional(function () use ($user, $provider, $clientID, $email) {
$this->insertSQL(
'users_provider_ids',
[
'id_user' => $user->id,
'provider' => $provider,
'client_id' => $clientID,
'email' => $email,
]
);
return $this->getUserByClientID($provider, $clientID);
});
if ($token?->getUser()) {
if ($user) {
addUserMessage(
sprintf(
translate('oauth', 'login')['bind_success'],
ucfirst($provider),
$email
),
'success'
);
if ($resourceOwner instanceof ICustomResourceOwner) {
$resourceOwner->updateUserData($user, $response);
}
throw new OauthBindSuccessException();
} else {
throw new OauthBindFailedException('User not found.');
}
}
} catch (UniqueConstraintViolationException $e) {
throw new OauthBindFailedException('OAuth already binded.');
}
}
}
// add new user
if (!$user && !empty($email) && !empty($response->getFirstName()) && !empty($response->getLastName())
&& !empty($clientID)
) {
try {
$user = sqlGetConnection()->transactional(function () use ($email, $response, $provider, $clientID) {
$insert = [
'email' => $email,
'name' => $response->getFirstName(),
'surname' => $response->getLastName(),
'date_reg' => date('Y-m-d H:i:s'),
];
if (findModule(\Modules::CURRENCIES)) {
$currencyContext = Contexts::get(CurrencyContext::class);
$insert['currency'] = $currencyContext->getActiveId();
}
if (findModule(\Modules::TRANSLATIONS)) {
$languageContext = Contexts::get(LanguageContext::class);
$insert['id_language'] = $languageContext->getActiveId();
}
try {
$this->insertSQL('users', $insert);
$userId = sqlInsertId();
} catch (UniqueConstraintViolationException) {
if (!($userId = $this->checkNewsletterUser($insert['email'], $insert))) {
// pokud uzivatel existuje a je neaktivni z jineho duvodu, nez newsletter, tak ho nedovolim sparovat/prihlasit
throw new UserNotFoundException();
}
}
$this->insertSQL(
'users_provider_ids',
[
'id_user' => $userId,
'provider' => $provider,
'client_id' => $clientID,
'email' => $email,
]
);
$this->eventDispatcher->dispatch(
new UserRegisteredEvent(\User::createFromId($userId), $response->getResourceOwner()->getName())
);
return $this->getUserByClientID($provider, $clientID);
});
// redirect to user account
addUserMessage(translate('oauth', 'login')['registration_success'], 'success');
$this->requestStack->getMainRequest()->attributes->set('gtm_registration', [
'email' => $email ?? 'no-email',
'firstname' => $response->getFirstName() ?? 'no-firstname',
]);
// force redirection to user settings
$this->session->set('_security.main.target_path', path('settings'));
} catch (UniqueConstraintViolationException $e) {
}
}
// user was not found by clientID, but we already have his email, so we can't register him
// LoginView catches this error and displays translatable message:
// translate('oauth', 'login')['unknown_clientid_with_known_email']
if (!$user) {
throw new ClientIDNotFoundException($email ?? '');
}
if ($resourceOwner instanceof ICustomResourceOwner) {
$resourceOwner->updateUserData($user, $response);
}
if ($this->session->get('oauth_register_redirect')) {
$this->session->set('_security.main.target_path', $this->session->get('oauth_register_redirect'));
$this->session->remove('oauth_register_redirect');
}
// user found or successfully created
return new OauthUser($user->id, $user->email, $provider, $clientID, ['ROLE_USER'], $user);
}
private function checkNewsletterUser(string $email, array $update = []): ?int
{
$user = sqlQueryBuilder()
->select('id, figure, passw, get_news, date_subscribe, date_unsubscribe')
->from('users')
->where(Operator::equals(['email' => $email]))
->execute()->fetchAssociative();
if (!$user) {
return null;
}
// pokud je to neaktivni uzivatel, kterej nejspis vznikl pres newsletter, tak ho rovnou zaktivuju at to prihlaseni pres socky vubec funguje
if ($user['figure'] === 'N' && empty($user['passw']) && ($user['get_news'] === 'Y' || !empty($user['date_subscribe']) || !empty($user['date_unsubscribe']))) {
sqlQueryBuilder()
->update('users')
->directValues(array_merge($update, ['figure' => 'Y']))
->where(Operator::equals(['id' => $user['id']]))
->execute();
} else {
// pokud to neni newsletter uzivatel, tak vracim null
return null;
}
return $user['id'];
}
/**
* @required
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\UserOauthBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class ShouldLogOutException extends AccessDeniedException
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace KupShop\UserOauthBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class UserOauthBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new DependencyInjection\Compiler\ResourceOwnerPass());
}
}