Added proper OAuth authentication for digikey and other providers

This commit is contained in:
Jan Böhmer 2023-07-16 03:07:53 +02:00
parent a95ba1acc4
commit c203de082e
13 changed files with 876 additions and 19 deletions

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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');
}
}

View file

@ -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 [];
}
}

View file

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

View file

@ -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();

View file

@ -0,0 +1,128 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();
}
}