Skip to content

Commit

Permalink
add access token refreshing logic (#410)
Browse files Browse the repository at this point in the history
* add access token refreshing logic

* styleci

* refactor: extract token interaction logic into trait

* integrate access token refreshing with per-character scheduling
  • Loading branch information
recursivetree authored Sep 6, 2024
1 parent 6be25bf commit 0c87b92
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 38 deletions.
5 changes: 2 additions & 3 deletions src/Commands/Seat/Buckets/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use Illuminate\Support\Facades\Cache;
use Seat\Eveapi\Bus\Character;
use Seat\Eveapi\Bus\Corporation;
use Seat\Eveapi\Jobs\Character\Roles;
use Seat\Eveapi\Jobs\Token\RefreshAccessToken;
use Seat\Eveapi\Models\Bucket;
use Seat\Eveapi\Models\RefreshToken;
use Seat\Eveapi\Models\RefreshTokenSchedule;
Expand Down Expand Up @@ -140,8 +140,7 @@ private function dispatchCharacterEsiUpdate(RefreshToken $token): void
*/
private function dispatchCharacterTokenKeepAlive(RefreshToken $token): void
{
// TODO: add a job that only requests a new access token instead of a random esi job. This will require some eseye rework
Roles::dispatch($token)->onQueue('characters');
RefreshAccessToken::dispatch($token)->onQueue('characters');
}

/**
Expand Down
72 changes: 72 additions & 0 deletions src/InteractsWithToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?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\Eveapi;

use Seat\Eveapi\Models\RefreshToken;
use Seat\Services\Contracts\EsiClient;

trait InteractsWithToken
{
abstract protected function getClient(): EsiClient;

abstract protected function getToken(): ?RefreshToken;

/**
* @return void
*/
protected function configureTokenForEsiClient(): void
{
$token = $this->getToken();
if($token !== null) {
$this->getClient()->setAuthentication($token);
}
}

/**
* Update the access_token last used in the job,
* along with the expiry time.
*
* @return void
*/
public function updateRefreshToken(): void
{
$client = $this->getClient();
$token = $this->getToken();

// If it is an unauthenticated call, there is nothing to update
if (is_null($token))
return;

if (! $client->isAuthenticated())
return;

$last_auth = $client->getAuthentication();

// update the token
if (! empty($last_auth->getRefreshToken()))
$token->refresh_token = $last_auth->getRefreshToken();
$token->token = $last_auth->getAccessToken() ?? '-';
$token->expires_on = $last_auth->getExpiresOn();
$token->save();
}
}
45 changes: 10 additions & 35 deletions src/Jobs/EsiBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Seat\Eveapi\Exception\PermanentInvalidTokenException;
use Seat\Eveapi\Exception\TemporaryEsiOutageException;
use Seat\Eveapi\Exception\UnavailableEveServersException;
use Seat\Eveapi\InteractsWithToken;
use Seat\Eveapi\Jobs\Middleware\CheckEsiRateLimit;
use Seat\Eveapi\Jobs\Middleware\CheckEsiRouteStatus;
use Seat\Eveapi\Jobs\Middleware\CheckServerStatus;
Expand All @@ -46,6 +47,8 @@
*/
abstract class EsiBase extends AbstractJob
{
use InteractsWithToken;

/**
* ANTI_RACE_DELAY prevents rapid job recycling with low queue depths.
*/
Expand Down Expand Up @@ -212,6 +215,11 @@ public function getToken(): ?RefreshToken
return $this->token;
}

protected function getClient(): EsiClient
{
return $this->esi;
}

/**
* @return string
*/
Expand Down Expand Up @@ -299,17 +307,13 @@ public function retrieve(array $path_values = []): EsiResponse
if (! is_null($this->page))
$this->esi->page($this->page);

$this->configureTokenForEsiClient();

// Generally, we want to bubble up exceptions all the way to the
// callee. However, in the case of this worker class, we need to
// try and be vigilant with tokens that may have expired. So for
// those cases we wrap in a try/catch.
try {
if ($this->token) {
$this->token = $this->token->fresh();

$this->esi->setAuthentication($this->token);
}

$result = $this->esi->invoke($this->method, $this->endpoint, $path_values);

// Update the refresh token we have stored in the database.
Expand Down Expand Up @@ -398,35 +402,6 @@ public function warning(EsiResponse $response): void
}
}

/**
* Update the access_token last used in the job,
* along with the expiry time.
*/
public function updateRefreshToken(): void
{

tap($this->token, function ($token) {

// If no API call was made, the client would have never
// been instantiated and auth information never updated.
if (is_null($token))
return;

if (! $this->esi->isAuthenticated())
return;

$last_auth = $this->esi->getAuthentication();

if (! empty($last_auth->getRefreshToken()))
$token->refresh_token = $last_auth->getRefreshToken();

$token->token = $last_auth->getAccessToken() ?? '-';
$token->expires_on = $last_auth->getExpiresOn();

$token->save();
});
}

/**
* Check if there are any pages left in a response
* based on the number of pages available and the
Expand Down
88 changes: 88 additions & 0 deletions src/Jobs/Token/RefreshAccessToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?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\Eveapi\Jobs\Token;

use Illuminate\Contracts\Container\BindingResolutionException;
use Seat\Eseye\Exceptions\InvalidContainerDataException;
use Seat\Eveapi\InteractsWithToken;
use Seat\Eveapi\Jobs\AbstractJob;
use Seat\Eveapi\Models\RefreshToken;
use Seat\Services\Contracts\EsiClient;

class RefreshAccessToken extends AbstractJob
{
use InteractsWithToken;

/**
* @var array
*/
protected $tags = ['character', 'token'];

/**
* @var \Seat\Eveapi\Models\RefreshToken
*/
protected $token;

/**
* @var EsiClient
*/
protected EsiClient $esi;

/**
* @param RefreshToken $token
*
* @throws BindingResolutionException
*/
public function __construct(RefreshToken $token)
{
$this->token = $token;
$this->esi = app()->make(EsiClient::class);
}

/**
* @return void
*
* @throws InvalidContainerDataException
*/
public function handle(): void
{
// pass this token to the esi client
$this->configureTokenForEsiClient();

// ensure we have a valid access token
$this->esi->getValidAccessToken();

// make sure the new token is stored
$this->updateRefreshToken();
}

public function getClient(): EsiClient
{
return $this->esi;
}

public function getToken(): ?RefreshToken
{
return $this->token;
}
}
11 changes: 11 additions & 0 deletions src/Services/EseyeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
use Seat\Eseye\Configuration;
use Seat\Eseye\Containers\EsiAuthentication;
use Seat\Eseye\Eseye;
use Seat\Eseye\Exceptions\InvalidAuthenticationException;
use Seat\Eseye\Exceptions\InvalidContainerDataException;
use Seat\Services\Contracts\EsiClient;
use Seat\Services\Contracts\EsiResponse;
use Seat\Services\Contracts\EsiToken;
Expand Down Expand Up @@ -227,4 +229,13 @@ public function getCache(): CacheInterface
{
return $this->instance->getConfiguration()->getCache();
}

/**
* @throws InvalidAuthenticationException
* @throws InvalidContainerDataException
*/
public function getValidAccessToken(): string
{
return $this->instance->getValidAccessToken()->access_token;
}
}

0 comments on commit 0c87b92

Please sign in to comment.