From c203de082e63932ef3dda23e08d966be4836a40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 16 Jul 2023 03:07:53 +0200 Subject: [PATCH] Added proper OAuth authentication for digikey and other providers --- composer.json | 1 + composer.lock | 551 +++++++++++++++++- config/bundles.php | 1 + config/packages/http_client.yaml | 14 +- config/packages/knpu_oauth2_client.yaml | 18 + config/packages/nelmio_security.yaml | 3 + config/services.yaml | 5 + src/Controller/OAuthClientController.php | 63 ++ src/Entity/OAuthToken.php | 73 ++- .../LogSystem/EventLoggerSubscriber.php | 6 + .../Providers/DigikeyProvider.php | 20 +- src/Services/OAuth/OAuthTokenManager.php | 128 ++++ symfony.lock | 12 + 13 files changed, 876 insertions(+), 19 deletions(-) create mode 100644 config/packages/knpu_oauth2_client.yaml create mode 100644 src/Controller/OAuthClientController.php create mode 100644 src/Services/OAuth/OAuthTokenManager.php diff --git a/composer.json b/composer.json index da6af5d7..fd96da4b 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "jbtronics/2fa-webauthn": "^v2.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jfcherng/php-diff": "^6.14", + "knpuniversity/oauth2-client-bundle": "^2.15", "league/csv": "^9.8.0", "league/html-to-markdown": "^5.0.1", "liip/imagine-bundle": "^2.2", diff --git a/composer.lock b/composer.lock index 46bc8e70..99333e7d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2996892a0aeaa1363a3d9643551d2e58", + "content-hash": "1c3a6a5bba2865b104630aaf4336e483", "packages": [ { "name": "beberlei/assert", @@ -2393,6 +2393,331 @@ }, "time": "2022-01-11T08:28:06+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:04:53+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6", + "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T13:50:22+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:11:26+00:00" + }, { "name": "imagine/imagine", "version": "1.3.5", @@ -2806,6 +3131,66 @@ ], "time": "2023-05-21T07:57:08+00:00" }, + { + "name": "knpuniversity/oauth2-client-bundle", + "version": "v2.15.0", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "9df0736d02eb20b953ec8e9986743611747d9ed9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/9df0736d02eb20b953ec8e9986743611747d9ed9", + "reference": "9df0736d02eb20b953ec8e9986743611747d9ed9", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=7.4", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "league/oauth2-facebook": "^1.1|^2.0", + "phpstan/phpstan": "^0.12", + "symfony/phpunit-bridge": "^5.3.1|^6.0", + "symfony/security-guard": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/security-guard": "For integration with Symfony's Guard Security layer" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "KnpU\\OAuth2ClientBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Weaver", + "email": "ryan@symfonycasts.com" + } + ], + "description": "Integration with league/oauth2-client to provide services", + "homepage": "https://symfonycasts.com", + "keywords": [ + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.15.0" + }, + "time": "2023-05-03T16:44:38+00:00" + }, { "name": "laminas/laminas-code", "version": "4.11.0", @@ -3181,6 +3566,76 @@ ], "time": "2023-07-12T21:21:09+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + }, + "time": "2023-04-16T18:19:15+00:00" + }, { "name": "liip/imagine-bundle", "version": "2.11.0", @@ -4197,6 +4652,56 @@ }, "time": "2022-06-14T06:56:20+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "part-db/label-fonts", "version": "v1.0.0", @@ -5533,6 +6038,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "robrichards/xmlseclibs", "version": "3.1.1", diff --git a/config/bundles.php b/config/bundles.php index 89a63165..6545338d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -30,4 +30,5 @@ return [ Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true], Jbtronics\DompdfFontLoaderBundle\DompdfFontLoaderBundle::class => ['all' => true], + KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], ]; diff --git a/config/packages/http_client.yaml b/config/packages/http_client.yaml index 9489e92b..2e693f7f 100644 --- a/config/packages/http_client.yaml +++ b/config/packages/http_client.yaml @@ -2,16 +2,4 @@ framework: http_client: default_options: headers: - 'User-Agent': 'Part-DB' - - - scoped_clients: - digikey.client: - base_uri: 'https://sandbox-api.digikey.com' - auth_bearer: '%env(PROVIDER_DIGIKEY_TOKEN)%' - headers: - X-DIGIKEY-Client-Id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%' - X-DIGIKEY-Locale-Site: 'DE' - X-DIGIKEY-Locale-Language: 'de' - X-DIGIKEY-Locale-Currency: '%partdb.default_currency%' - X-DIGIKEY-Customer-Id: 0 \ No newline at end of file + 'User-Agent': 'Part-DB' \ No newline at end of file diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml new file mode 100644 index 00000000..c59fe206 --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,18 @@ +knpu_oauth2_client: + clients: + # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration + + ip_digikey_oauth: + type: generic + provider_class: '\League\OAuth2\Client\Provider\GenericProvider' + + client_id: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%' + client_secret: '%env(PROVIDER_DIGIKEY_SECRET)%' + + redirect_route: 'oauth_client_check' + redirect_params: {name: 'ip_digikey_oauth'} + + provider_options: + urlAuthorize: 'https://sandbox-api.digikey.com/v1/oauth2/authorize' + urlAccessToken: 'https://sandbox-api.digikey.com/v1/oauth2/token' + urlResourceOwnerDetails: '' diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml index c8d24af0..c12fdb8b 100644 --- a/config/packages/nelmio_security.yaml +++ b/config/packages/nelmio_security.yaml @@ -16,6 +16,9 @@ nelmio_security: # Whitelist the domain of the SAML IDP, so we can redirect to it during the SAML login process - '%env(string:key:host:url:SAML_IDP_SINGLE_SIGN_ON_SERVICE)%' + # Whitelist the info provider APIs + - 'digikey.com' + # forces Microsoft's XSS-Protection with # its block mode xss_protection: diff --git a/config/services.yaml b/config/services.yaml index 54807566..a026f63d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -248,6 +248,11 @@ services: arguments: $api_key: '%env(PROVIDER_ELEMENT14_KEY)%' + App\Services\InfoProviderSystem\Providers\DigikeyProvider: + arguments: + $clientId: '%env(PROVIDER_DIGIKEY_CLIENT_ID)%' + $currency: '%partdb.default_currency%' + App\Services\InfoProviderSystem\Providers\TMEClient: arguments: $secret: '%env(PROVIDER_TME_SECRET)%' diff --git a/src/Controller/OAuthClientController.php b/src/Controller/OAuthClientController.php new file mode 100644 index 00000000..0b80a324 --- /dev/null +++ b/src/Controller/OAuthClientController.php @@ -0,0 +1,63 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Controller; + +use App\Services\OAuth\OAuthTokenManager; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +use function Symfony\Component\Translation\t; + +#[Route('/oauth/client')] +class OAuthClientController extends AbstractController +{ + public function __construct(private readonly ClientRegistry $clientRegistry, private readonly OAuthTokenManager $tokenManager) + { + + } + + #[Route('/{name}/connect', name: 'oauth_client_connect')] + public function connect(string $name): Response + { + return $this->clientRegistry + ->getClient($name) // key used in config/packages/knpu_oauth2_client.yaml + ->redirect(); + } + + #[Route('/{name}/check', name: 'oauth_client_check')] + public function check(string $name, Request $request): Response + { + $client = $this->clientRegistry->getClient($name); + + $access_token = $client->getAccessToken(); + $this->tokenManager->saveToken($name, $access_token); + + $this->addFlash('success', t('oauth_client.flash.connection_successful')); + + return $this->redirectToRoute('homepage'); + } +} \ No newline at end of file diff --git a/src/Entity/OAuthToken.php b/src/Entity/OAuthToken.php index 9fbacb46..30a8feef 100644 --- a/src/Entity/OAuthToken.php +++ b/src/Entity/OAuthToken.php @@ -27,6 +27,7 @@ use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use League\OAuth2\Client\Token\AccessTokenInterface; /** * This entity represents a OAuth token pair (access and refresh token), for an application @@ -35,7 +36,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Table(name: 'oauth_tokens')] #[ORM\UniqueConstraint(name: 'oauth_tokens_unique_name', columns: ['name'])] #[ORM\Index(columns: ['name'], name: 'oauth_tokens_name_idx')] -class OAuthToken extends AbstractNamedDBElement +class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface { /** @var string|null The short-term usable OAuth2 token */ #[ORM\Column(type: 'string', nullable: true)] @@ -49,10 +50,10 @@ class OAuthToken extends AbstractNamedDBElement #[ORM\Column(type: 'string')] private string $refresh_token = ''; + private const DEFAULT_EXPIRATION_TIME = 3600; + public function __construct(string $name, string $refresh_token, string $token = null, \DateTimeInterface $expires_at = null) { - parent::__construct(); - //If token is given, you also have to give the expires_at date if ($token !== null && $expires_at === null) { throw new \InvalidArgumentException('If you give a token, you also have to give the expires_at date'); @@ -64,4 +65,70 @@ class OAuthToken extends AbstractNamedDBElement $this->token = $token; } + public static function fromAccessToken(AccessTokenInterface $accessToken, string $name): self + { + return new self( + $name, + $accessToken->getRefreshToken(), + $accessToken->getToken(), + self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME) + ); + } + + private static function unixTimestampToDatetime(int $timestamp): \DateTimeInterface + { + return \DateTimeImmutable::createFromFormat('U', (string)$timestamp); + } + + public function getToken(): ?string + { + return $this->token; + } + + public function getExpirationDate(): ?\DateTimeInterface + { + return $this->expires_at; + } + + public function getRefreshToken(): string + { + return $this->refresh_token; + } + + public function isExpired(): bool + { + //null token is always expired + if ($this->token === null) { + return true; + } + + if ($this->expires_at === null) { + return false; + } + + return $this->expires_at->getTimestamp() < time(); + } + + public function replaceWithNewToken(AccessTokenInterface $accessToken): void + { + $this->token = $accessToken->getToken(); + $this->refresh_token = $accessToken->getRefreshToken(); + //If no expiration date is given, we set it to the default expiration time + $this->expires_at = self::unixTimestampToDatetime($accessToken->getExpires() ?? time() + self::DEFAULT_EXPIRATION_TIME); + } + + public function getExpires() + { + return $this->expires_at->getTimestamp(); + } + + public function hasExpired() + { + return $this->isExpired(); + } + + public function getValues() + { + return []; + } } \ No newline at end of file diff --git a/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php b/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php index f48cf749..5a7fd50a 100644 --- a/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php +++ b/src/EventSubscriber/LogSystem/EventLoggerSubscriber.php @@ -29,6 +29,7 @@ use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Entity\OAuthToken; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\PartLot; use App\Entity\PriceInformations\Orderdetail; @@ -344,6 +345,11 @@ class EventLoggerSubscriber implements EventSubscriber */ protected function validEntity(object $entity): bool { + //Dont log OAuthTokens + if ($entity instanceof OAuthToken) { + return false; + } + //Dont log logentries itself! return $entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry; } diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index 26cea80c..b22b18d6 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -26,14 +26,29 @@ namespace App\Services\InfoProviderSystem\Providers; use App\Entity\Parts\ManufacturingStatus; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\OAuth\OAuthTokenManager; use Symfony\Contracts\HttpClient\HttpClientInterface; class DigikeyProvider implements InfoProviderInterface { - public function __construct(private readonly HttpClientInterface $digikeyClient) - { + private const OAUTH_APP_NAME = 'ip_digikey_oauth'; + private readonly HttpClientInterface $digikeyClient; + + public function __construct(HttpClientInterface $httpClient, private readonly OAuthTokenManager $authTokenManager, string $currency, string $clientId) + { + //Create the HTTP client with some default options + $this->digikeyClient = $httpClient->withOptions([ + "base_uri" => 'https://sandbox-api.digikey.com', + "headers" => [ + "X-DIGIKEY-Client-Id" => $clientId, + "X-DIGIKEY-Locale-Site" => 'DE', + "X-DIGIKEY-Locale-Language" => 'de', + "X-DIGIKEY-Locale-Currency" => $currency, + "X-DIGIKEY-Customer-Id" => 0, + ] + ]); } public function getProviderInfo(): array @@ -77,6 +92,7 @@ class DigikeyProvider implements InfoProviderInterface $response = $this->digikeyClient->request('POST', '/Search/v3/Products/Keyword', [ 'json' => $request, + 'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME) ]); $response_array = $response->toArray(); diff --git a/src/Services/OAuth/OAuthTokenManager.php b/src/Services/OAuth/OAuthTokenManager.php new file mode 100644 index 00000000..1e76c8d0 --- /dev/null +++ b/src/Services/OAuth/OAuthTokenManager.php @@ -0,0 +1,128 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\OAuth; + +use App\Entity\OAuthToken; +use Doctrine\ORM\EntityManagerInterface; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use League\OAuth2\Client\Token\AccessTokenInterface; + +final class OAuthTokenManager +{ + public function __construct(private readonly ClientRegistry $clientRegistry, private readonly EntityManagerInterface $entityManager) + { + + } + + /** + * Saves the given token to the database, so it can be retrieved later + * @param string $app_name + * @param AccessTokenInterface $token + * @return void + */ + public function saveToken(string $app_name, AccessTokenInterface $token): void + { + //Check if we already have a token for this app + $tokenEntity = $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]); + + //If the token was already existing, we just replace it with the new one + if ($tokenEntity) { + $tokenEntity->replaceWithNewToken($token); + + $this->entityManager->flush($tokenEntity); + + //We are done + return; + } + + //If the token was not existing, we create a new one + $tokenEntity = OAuthToken::fromAccessToken($token, $app_name); + $this->entityManager->persist($tokenEntity); + $this->entityManager->flush($tokenEntity); + + return; + } + + /** + * Returns the token for the given app name + * @param string $app_name + * @return OAuthToken|null + */ + public function getToken(string $app_name): ?OAuthToken + { + return $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]); + } + + /** + * This function refreshes the token for the given app name. The new token is saved to the database + * The app_name must be registered in the knpu_oauth2_client.yaml + * @param string $app_name + * @return OAuthToken + * @throws \Exception + */ + public function refreshToken(string $app_name): OAuthToken + { + $token = $this->getToken($app_name); + + if (!$token) { + throw new \Exception('No token was saved yet for '.$app_name); + } + + $client = $this->clientRegistry->getClient($app_name); + $new_token = $client->refreshAccessToken($token->getRefreshToken()); + + //Persist the token + $token->replaceWithNewToken($new_token); + $this->entityManager->flush($token); + + return $token; + } + + /** + * This function returns the token of the given app name + * @param string $app_name + * @return OAuthToken|null + */ + public function getAlwaysValidTokenString(string $app_name): ?string + { + //Get the token for the application + $token = $this->getToken($app_name); + + //If the token is not existing, we return null + if (!$token) { + return null; + } + + //If the token is still valid, we return it + if (!$token->hasExpired()) { + return $token->getToken(); + } + + //If the token is expired, we refresh it + $this->refreshToken($app_name); + + //And return the new token + return $token->getToken(); + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index ed92f4e1..d47e131c 100644 --- a/symfony.lock +++ b/symfony.lock @@ -180,6 +180,18 @@ "jbtronics/dompdf-font-loader-bundle": { "version": "dev-main" }, + "knpuniversity/oauth2-client-bundle": { + "version": "2.15", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.20", + "ref": "1ff300d8c030f55c99219cc55050b97a695af3f6" + }, + "files": [ + "./config/packages/knpu_oauth2_client.yaml" + ] + }, "laminas/laminas-code": { "version": "3.4.1" },