Skip to content

Commit

Permalink
[4.x] Automatically refresh expired tokens (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
rennokki authored Feb 24, 2023
1 parent a0025b7 commit 89bf059
Show file tree
Hide file tree
Showing 32 changed files with 948 additions and 193 deletions.
1 change: 0 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ The function accepts a callback so if you wanted to implement more complex logic

V3 introduces a new `Providers` class, for defining what Socialite providers you have enabled in your config. This class is also used in the socialstream.blade.php stub and the connected-account.blade.php component stub. Please update any Socialite providers you have in your `socialstream.php` config file to use this class, e.g:


```php
use \JoelButcher\Socialstream\Providers;

Expand Down
1 change: 1 addition & 0 deletions config/socialstream.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
// Features::generateMissingEmails(),
Features::rememberSession(),
Features::providerAvatars(),
Features::refreshOauthTokens(),
],
];
66 changes: 66 additions & 0 deletions src/Concerns/RefreshesOauth2Tokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace JoelButcher\Socialstream\Concerns;

use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Arr;
use JoelButcher\Socialstream\ConnectedAccount;
use JoelButcher\Socialstream\RefreshedCredentials;
use Laravel\Socialite\Two\AbstractProvider;

/**
* @mixin AbstractProvider&RefreshesOauth2Tokens
*/
trait RefreshesOauth2Tokens
{
/**
* Refresh the token for the current provider.
*
* @throws GuzzleException
*/
public function refreshToken(ConnectedAccount $connectedAccount): RefreshedCredentials
{
if (is_null($connectedAccount->refresh_token)) {
throw new \RuntimeException('A valid refresh token is required.');
}

$response = $this->getHttpClient()->post($this->getTokenUrl(), [
RequestOptions::HEADERS => $this->getRefreshTokenHeaders(),
RequestOptions::FORM_PARAMS => $this->getRefreshTokenFields($connectedAccount->refresh_token),
]);

$response = json_decode($response->getBody(), true);

return new RefreshedCredentials(
token: Arr::get($response, 'access_token'),
refreshToken: Arr::get($response, 'refresh_token'),
expiry: now()->addSeconds(Arr::get($response, 'expires_in')),
);
}

/**
* Get the headers for the refresh token request.
*
* @return array<string, string>
*/
protected function getRefreshTokenHeaders(): array
{
return ['Accept' => 'application/json'];
}

/**
* Get the POST fields for the refresh token request.
*
* @return array<string, string>
*/
protected function getRefreshTokenFields(string $refreshToken): array
{
return [
'grant_type' => 'refresh_token',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'refresh_token' => $refreshToken,
];
}
}
8 changes: 3 additions & 5 deletions src/ConnectedAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@

namespace JoelButcher\Socialstream;

use DateTimeInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Jetstream\Jetstream;

/**
* @property int $id
* @property int $user_id
* @property string $provider
* @property string $provider_id
* @property string $token
* @property string|null $secret
* @property string|null $refresh_token
* @property DateTimeInterface|null $expires_at
*/
abstract class ConnectedAccount extends Model
{
use HasOauth2Tokens;

/**
* Get the credentials used for authenticating services.
*/
Expand Down
30 changes: 1 addition & 29 deletions src/Contracts/Credentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,12 @@

use DateTimeInterface;

interface Credentials
interface Credentials extends RefreshedCredentials
{
/**
* Get the ID for the credentials.
*
* @return string
*/
public function getId(): string;

/**
* Get token for the credentials.
*
* @return string
*/
public function getToken(): string;

/**
* Get the token secret for the credentials.
*
* @return string|null
*/
public function getTokenSecret(): ?string;

/**
* Get the refresh token for the credentials.
*
* @return string|null
*/
public function getRefreshToken(): ?string;

/**
* Get the expiry date for the credentials.
*
* @return DateTimeInterface|null
*/
public function getExpiry(): ?DateTimeInterface;
}
14 changes: 14 additions & 0 deletions src/Contracts/Oauth2RefreshResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace JoelButcher\Socialstream\Contracts;

use JoelButcher\Socialstream\ConnectedAccount;
use JoelButcher\Socialstream\RefreshedCredentials;

interface Oauth2RefreshResolver
{
/**
* Refreshes the token for the current provider.
*/
public function refreshToken(ConnectedAccount $connectedAccount): RefreshedCredentials;
}
36 changes: 36 additions & 0 deletions src/Contracts/RefreshedCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace JoelButcher\Socialstream\Contracts;

use DateTimeInterface;

interface RefreshedCredentials
{
/**
* Get token for the credentials.
*
* @return string
*/
public function getToken();

/**
* Get the token secret for the credentials.
*
* @return string|null
*/
public function getTokenSecret();

/**
* Get the refresh token for the credentials.
*
* @return string|null
*/
public function getRefreshToken();

/**
* Get the expiry date for the credentials.
*
* @return DateTimeInterface|null
*/
public function getExpiry();
}
127 changes: 10 additions & 117 deletions src/Credentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,157 +2,50 @@

namespace JoelButcher\Socialstream;

use DateTime;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use JoelButcher\Socialstream\Contracts\Credentials as CredentialsContract;
use JsonSerializable;

class Credentials implements CredentialsContract, Arrayable, Jsonable, JsonSerializable
class Credentials extends RefreshedCredentials implements CredentialsContract, Arrayable, Jsonable, JsonSerializable
{
/**
* The credentials user ID.
*
* @var string
*/
protected string $id;

/**
* The credentials token.
*
* @var string
*/
protected string $token;

/**
* The credentials token secret.
*
* @var string|null
*/
protected string|null $tokenSecret;

/**
* The credentials refresh token.
*
* @var string|null
*/
protected string|null $refreshToken;

/**
* The credentials expiry.
*/
protected DateTimeInterface|null $expiry;

/**
* Create a new credentials instance.
*/
public function __construct(ConnectedAccount $connectedAccount)
{
$this->id = $connectedAccount->provider_id;
$this->token = $connectedAccount->token;
$this->tokenSecret = $connectedAccount->secret;
$this->refreshToken = $connectedAccount->refresh_token;
$this->expiry = $connectedAccount->expires_at;

parent::__construct(
$connectedAccount->token,
$connectedAccount->secret,
$connectedAccount->refresh_token,
$connectedAccount->expires_at,
);
}

/**
* Get the ID for the credentials.
*
* @return string
*/
public function getId(): string
{
return $this->id;
}

/**
* Get token for the credentials.
*
* @return string
*/
public function getToken(): string
{
return $this->token;
}

/**
* Get the token secret for the credentials.
*
* @return string|null
*/
public function getTokenSecret(): ?string
{
return $this->tokenSecret;
}

/**
* Get the refresh token for the credentials.
*
* @return string|null
*/
public function getRefreshToken(): ?string
{
return $this->refreshToken;
}

/**
* Get the expiry date for the credentials.
*
* @return DateTimeInterface|null
* @throws Exception
*/
public function getExpiry(): ?DateTimeInterface
{
if (is_null($this->expiry)) {
return null;
}

return new DateTime($this->expiry);
}

/**
* Get the instance as an array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
return array_merge([
'id' => $this->getId(),
'token' => $this->getToken(),
'token_secret' => $this->getTokenSecret(),
'refresh_token' => $this->getRefreshToken(),
'expiry' => $this->getExpiry(),
];
}

/**
* Convert the object to its JSON representation.
*
* @param int $options
*/
public function toJson($options = 0): string
{
return json_encode($this->toArray(), $options);
}

/**
* Specify data which should be serialized to JSON.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}

/**
* Convert the object instance to a string.
*/
public function __toString(): string
{
return json_encode($this->toJson());
], parent::toArray());
}
}
16 changes: 16 additions & 0 deletions src/Features.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ public static function hasRememberSessionFeatures(): bool
return static::enabled(static::rememberSession());
}

/**
* Determine if the application should refresh the tokens on retrieval.
*/
public static function refreshesOauthTokens(): bool
{
return static::enabled(static::refreshOauthTokens());
}

/**
* Enabled the generate missing emails feature.
*/
Expand Down Expand Up @@ -94,4 +102,12 @@ public static function rememberSession(): string
{
return 'remember-session';
}

/**
* Enable the automatic refresh token update on token retrieval.
*/
public static function refreshOauthTokens(): string
{
return 'refresh-oauth-tokens';
}
}
Loading

0 comments on commit 89bf059

Please sign in to comment.