Skip to content

Commit

Permalink
access token generation (#98)
Browse files Browse the repository at this point in the history
* introduce AccessTokenRefresher

* add access token resolving methods to eseye

* styleci
  • Loading branch information
recursivetree authored Sep 6, 2024
1 parent 28aaf90 commit 53f92a0
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 122 deletions.
197 changes: 197 additions & 0 deletions src/Access/AccessTokenRefresher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Eseye\Access;

use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use Seat\Eseye\Checker\EsiTokenValidator;
use Seat\Eseye\Configuration;
use Seat\Eseye\Containers\EsiAuthentication;
use Seat\Eseye\Containers\EsiResponse;
use Seat\Eseye\Eseye;
use Seat\Eseye\Exceptions\DiscoverServiceNotAvailableException;
use Seat\Eseye\Exceptions\InvalidAuthenticationException;
use Seat\Eseye\Exceptions\InvalidContainerDataException;
use Seat\Eseye\Exceptions\RequestFailedException;

class AccessTokenRefresher implements AccessTokenRefresherInterface
{
/**
* @var StreamFactoryInterface
*/
private StreamFactoryInterface $stream_factory;

/**
* @var RequestFactoryInterface
*/
private RequestFactoryInterface $request_factory;

/**
* @var string
*/
protected string $sso_base;

/**
* @var ClientInterface
*/
protected ClientInterface $client;

/**
* @var LoggerInterface
*/
protected LoggerInterface $logger;

/**
* @var EsiTokenValidator
*/
protected EsiTokenValidator $jwt_validator;

/**
* @throws InvalidContainerDataException
*/
public function __construct()
{
// Init the logger
$this->logger = Configuration::getInstance()->getLogger();

$this->client = Configuration::getInstance()->getHttpClient();
$this->stream_factory = Configuration::getInstance()->getHttpStreamFactory();
$this->request_factory = Configuration::getInstance()->getHttpRequestFactory();

// Init SSO base URI
$this->sso_base = sprintf('%s://%s:%d/v2/oauth',
Configuration::getInstance()->sso_scheme,
Configuration::getInstance()->sso_host,
Configuration::getInstance()->sso_port);

// Init JWT validator
$this->jwt_validator = new EsiTokenValidator();
}

/**
* @throws DiscoverServiceNotAvailableException
* @throws InvalidContainerDataException
* @throws RequestFailedException
* @throws ClientExceptionInterface
* @throws InvalidAuthenticationException
*/
public function getValidAccessToken(EsiAuthentication $authentication): EsiAuthentication
{
// Check the expiry date.
$expires = carbon($authentication->token_expires);

// If the token expires in the next minute, refresh it.
if ($expires->lte(carbon('now')->addMinute())) {
$authentication = $this->refreshToken($authentication);
}

return $authentication;
}

/**
* Refresh the Access token that we have in the EsiAccess container.
*
* @throws InvalidContainerDataException
* @throws ClientExceptionInterface
* @throws RequestFailedException
* @throws DiscoverServiceNotAvailableException
* @throws InvalidAuthenticationException
*/
private function refreshToken(EsiAuthentication $authentication): EsiAuthentication
{
// Make the post request for a new access_token
$stream = $this->stream_factory->createStream($this->getRefreshTokenForm($authentication));

$request = $this->request_factory->createRequest('POST', $this->sso_base . '/token')
->withHeader('Authorization', $this->getBasicAuthorizationHeader($authentication))
->withHeader('User-Agent', 'Eseye/' . Eseye::VERSION . '/' . Configuration::getInstance()->http_user_agent)
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
->withBody($stream);

$response = $this->client->sendRequest($request);

// Grab the body from the StreamInterface instance.
$content = $response->getBody()->getContents();

// Client or Server Exception
if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 600) {
// Log the event as failed
$this->logger->error('[http ' . $response->getStatusCode() . ', ' .
strtolower($response->getReasonPhrase()) . '] ' .
'get -> ' . $this->sso_base . '/token'
);

// For debugging purposes, log the response body
$this->logger->debug('Request for get -> ' . $this->sso_base . '/token failed. Response body was: ' .
$content);

// Raise the exception that should be handled by the caller
throw new RequestFailedException(new EsiResponse(
$content,
$response->getHeaders(),
'now',
$response->getStatusCode())
);
}

$json = json_decode($content);

$claims = $this->jwt_validator->validateToken($authentication->client_id, $json->access_token);
$this->logger->debug('Successfully validate delivered token', [
'claims' => $claims,
]);

// Set the new authentication values from the request
$authentication->access_token = $json->access_token;
$authentication->refresh_token = $json->refresh_token;
$authentication->token_expires = $claims['exp'];
$authentication->scopes = $claims['scp'];

return $authentication;
}

/**
* @param EsiAuthentication $authentication
* @return string
*/
private function getRefreshTokenForm(EsiAuthentication $authentication): string
{
$form = [
'grant_type' => 'refresh_token',
'refresh_token' => $authentication->refresh_token,
];

return http_build_query($form);
}

/**
* @return string
*/
private function getBasicAuthorizationHeader(EsiAuthentication $authentication): string
{
return 'Basic ' . base64_encode($authentication->client_id . ':' . $authentication->secret);
}
}
30 changes: 30 additions & 0 deletions src/Access/AccessTokenRefresherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of SeAT
*
* Copyright (C) 2015 to present Leon Jacobs
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

namespace Seat\Eseye\Access;

use Seat\Eseye\Containers\EsiAuthentication;

interface AccessTokenRefresherInterface
{
public function getValidAccessToken(EsiAuthentication $authentication): EsiAuthentication;
}
16 changes: 16 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Seat\Eseye\Access\AccessTokenRefresherInterface;
use Seat\Eseye\Containers\EsiConfiguration;
use Seat\Eseye\Exceptions\InvalidConfigurationException;

Expand Down Expand Up @@ -87,6 +88,11 @@ class Configuration
*/
protected RequestFactoryInterface|string|null $http_request_factory = null;

/**
* @var AccessTokenRefresherInterface|null
*/
protected AccessTokenRefresherInterface|null $access_token_refresher = null;

/**
* @var EsiConfiguration
*/
Expand Down Expand Up @@ -163,6 +169,16 @@ public function getCache(): CacheInterface
return $this->cache;
}

public function getAccessTokenRefresher(): AccessTokenRefresherInterface
{
if (! $this->access_token_refresher) {
$this->access_token_refresher = is_string($this->configuration->access_token_refresher) ?
new $this->configuration->access_token_refresher : $this->configuration->access_token_refresher;
}

return $this->access_token_refresher;
}

/**
* @return \Psr\Http\Client\ClientInterface
*/
Expand Down
48 changes: 25 additions & 23 deletions src/Containers/EsiConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

use GuzzleHttp\Psr7\HttpFactory;
use Psr\Http\Client\ClientInterface;
use Seat\Eseye\Access\AccessTokenRefresher;
use Seat\Eseye\Cache\NullCache;
use Seat\Eseye\Fetchers\Fetcher;
use Seat\Eseye\Log\NullLogger;
Expand Down Expand Up @@ -51,50 +52,51 @@ class EsiConfiguration extends AbstractArrayAccess
* @var array
*/
protected array $data = [
'http_user_agent' => 'Eseye Default Library',
'http_user_agent' => 'Eseye Default Library',

// Esi
'datasource' => 'tranquility',
'esi_scheme' => 'https',
'esi_host' => 'esi.evetech.net',
'esi_port' => 443,
'datasource' => 'tranquility',
'esi_scheme' => 'https',
'esi_host' => 'esi.evetech.net',
'esi_port' => 443,

// Eve Online SSO
'sso_scheme' => 'https',
'sso_host' => 'login.eveonline.com',
'sso_port' => 443,
'sso_scheme' => 'https',
'sso_host' => 'login.eveonline.com',
'sso_port' => 443,
'access_token_refresher' => AccessTokenRefresher::class,

// Fetcher
'fetcher' => Fetcher::class,
'fetcher' => Fetcher::class,

// Logging
'logger' => NullLogger::class,
'logger_level' => 'INFO',
'logfile_location' => 'logs/',
'logger' => NullLogger::class,
'logger_level' => 'INFO',
'logfile_location' => 'logs/',

// Rotating Logger Details
'log_max_files' => 10,
'log_max_files' => 10,

// Cache
'cache' => NullCache::class,
'cache' => NullCache::class,

// File Cache
'file_cache_location' => 'cache/',
'file_cache_location' => 'cache/',

// Redis Cache
'redis_cache_location' => 'tcp://127.0.0.1',
'redis_cache_prefix' => 'eseye:',
'redis_cache_location' => 'tcp://127.0.0.1',
'redis_cache_prefix' => 'eseye:',

// Memcached Cache
'memcached_cache_host' => '127.0.0.1',
'memcached_cache_port' => '11211',
'memcached_cache_prefix' => 'eseye:',
'memcached_cache_host' => '127.0.0.1',
'memcached_cache_port' => '11211',
'memcached_cache_prefix' => 'eseye:',
'memcached_cache_compressed' => false,

// HTTP
'http_client' => ClientInterface::class,
'http_request_factory' => HttpFactory::class,
'http_stream_factory' => HttpFactory::class,
'http_client' => ClientInterface::class,
'http_request_factory' => HttpFactory::class,
'http_stream_factory' => HttpFactory::class,
];

}
Loading

0 comments on commit 53f92a0

Please sign in to comment.