Skip to content

Commit

Permalink
Merge pull request #6 from platformsh/add-hooks-for-refresh-and-step-…
Browse files Browse the repository at this point in the history
…up-auth

Add hooks for the refresh cycle and step-up authentication
  • Loading branch information
pjcdawkins authored Nov 25, 2024
2 parents 963fe99 + e659251 commit 2c939a7
Showing 1 changed file with 140 additions and 23 deletions.
163 changes: 140 additions & 23 deletions src/GuzzleMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Platformsh\OAuth2\Client;

use GuzzleHttp\Exception\BadResponseException;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Grant\ClientCredentials;
use League\OAuth2\Client\Grant\RefreshToken;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -18,20 +20,32 @@ class GuzzleMiddleware
/** @var AbstractGrant $grant */
private $grant;

/** @var \League\OAuth2\Client\Token\AccessToken|null */
/** @var AccessToken|null */
private $accessToken;

/** @var array */
private $grantOptions = [];
private $grantOptions;

/** @var callable|null */
private $tokenSave;

/** @var callable|null */
protected $onRefreshStart;

/** @var callable|null */
protected $onRefreshEnd;

/** @var callable|null */
protected $onRefreshError;

/** @var callable|null */
protected $onStepUpAuthResponse;

/**
* GuzzleMiddleware constructor.
*
* @param \League\OAuth2\Client\Provider\AbstractProvider $provider
* @param \League\OAuth2\Client\Grant\AbstractGrant $grant
* @param AbstractProvider $provider
* @param AbstractGrant $grant
* @param array $grantOptions
*/
public function __construct(AbstractProvider $provider, AbstractGrant $grant = null, array $grantOptions = [])
Expand All @@ -53,6 +67,54 @@ public function setTokenSaveCallback(callable $tokenSave)
$this->tokenSave = $tokenSave;
}

/**
* Sets a callback that will be triggered when token refresh starts.
*
* @param callable $callback
* A callback which accepts 1 argument, the refresh token being used if
* available (a string or null), and returns an AccessToken or null.
*/
public function setOnRefreshStart(callable $callback)
{
$this->onRefreshStart = $callback;
}

/**
* Set a callback that will be triggered when token refresh ends.
*
* @param callable $callback
* A callback which accepts 1 argument, the refresh token which was used
* if available (a string or null).
*/
public function setOnRefreshEnd(callable $callback)
{
$this->onRefreshEnd = $callback;
}

/**
* Set a callback that will react to a refresh token error.
*
* @param callable $callback
* A callback which accepts one argument, the BadResponseException, and
* returns an AccessToken or null.
*/
public function setOnRefreshError(callable $callback)
{
$this->onRefreshError = $callback;
}

/**
* Set a callback that will react to a step-up authentication response (RFC 9470).
*
* @param callable $callback
* A callback which accepts one argument, the response, of type \GuzzleHttp\Message\ResponseInterface,
* and returns an AccessToken or null.
*/
public function setOnStepUpAuthResponse(callable $callback)
{
$this->onStepUpAuthResponse = $callback;
}

/**
* Main middleware callback.
*
Expand All @@ -73,23 +135,42 @@ public function __invoke(callable $next)
/** @var \GuzzleHttp\Promise\PromiseInterface $promise */
$promise = $next($request, $options);

return $promise->then(
function (ResponseInterface $response) use ($request, $options, $token, $next) {
if ($response->getStatusCode() === 401) {
// Consider the old token invalid, and get a new one.
$token = $this->getAccessToken($token);
return $promise->then(function (ResponseInterface $response) use ($request, $options, $token, $next) {
if ($response->getStatusCode() !== 401) {
return $response;
}

// Retry the request.
$request = $this->authenticateRequest($request, $token);
$response = $next($request, $options);
if (isset($this->onStepUpAuthResponse) && $this->isStepUpAuthenticationResponse($response)) {
$newToken = call_user_func($this->onStepUpAuthResponse, $response);
$this->accessToken = $newToken;
if (is_callable($this->tokenSave)) {
call_user_func($this->tokenSave, $this->accessToken);
}

return $response;
} else {
// Consider the old token invalid, and get a new one.
$this->getAccessToken($token);
}
);

// Retry the request.
$request = $this->authenticateRequest($request, $token);
return $next($request, $options);
});
};
}

/**
* Checks for a step-up authentication response (RFC 9470).
*
* @param ResponseInterface $response
*
* @return bool
*/
protected function isStepUpAuthenticationResponse(ResponseInterface $response)
{
$authHeader = implode("\n", $response->getHeader('WWW-Authenticate'));
return stripos($authHeader, 'Bearer') !== false && strpos($authHeader, 'insufficient_user_authentication') !== false;
}

/**
* Check if a request is configured to use OAuth2.
*
Expand All @@ -116,10 +197,10 @@ private function isOAuth2(RequestInterface $request, array $options)
/**
* Add authentication to an HTTP request.
*
* @param \Psr\Http\Message\RequestInterface $request
* @param \League\OAuth2\Client\Token\AccessToken $token
* @param RequestInterface $request
* @param AccessToken $token
*
* @return \Psr\Http\Message\RequestInterface
* @return RequestInterface
*/
private function authenticateRequest(RequestInterface $request, AccessToken $token)
{
Expand All @@ -131,13 +212,14 @@ private function authenticateRequest(RequestInterface $request, AccessToken $tok
}

/**
* Get the current access token.
* Get the current or a new access token.
*
* @param AccessToken|null $invalid
* A token to consider invalid.
*
* @return \League\OAuth2\Client\Token\AccessToken
* @return AccessToken
* The OAuth2 access token.
* @throws IdentityProviderException
*/
private function getAccessToken(AccessToken $invalid = null)
{
Expand All @@ -155,23 +237,58 @@ private function getAccessToken(AccessToken $invalid = null)
* Acquire a new access token using a refresh token or the configured grant.
*
* @return AccessToken
* @throws IdentityProviderException
*/
private function acquireAccessToken()
{
if (isset($this->accessToken) && $this->accessToken->getRefreshToken()) {
return $this->provider->getAccessToken(new RefreshToken(), ['refresh_token' => $this->accessToken->getRefreshToken()]);
$currentRefreshToken = $this->accessToken->getRefreshToken();
try {
if (isset($this->onRefreshStart)) {
$result = call_user_func($this->onRefreshStart, $currentRefreshToken);
if ($result instanceof AccessToken) {
return $result;
}
}
return $this->provider->getAccessToken(new RefreshToken(), ['refresh_token' => $this->accessToken->getRefreshToken()]);
} catch (BadResponseException $e) {
if (isset($this->onRefreshError)) {
$accessToken = call_user_func($this->onRefreshError, $e);
if ($accessToken) {
return $accessToken;
}
}
throw $e;
} finally {
if (isset($this->onRefreshEnd)) {
call_user_func($this->onRefreshEnd, $currentRefreshToken);
}
}
}

return $this->provider->getAccessToken($this->grant, $this->grantOptions);
}

/**
* Set the access token for the next request.
* Set the access token for the next request(s).
*
* @param \League\OAuth2\Client\Token\AccessToken $token
* @param AccessToken $token
*/
public function setAccessToken(AccessToken $token)
{
$this->accessToken = $token;
}

/**
* Set the access token for the next request(s), and save it to storage.
*
* @param AccessToken $token
*/
public function saveAccessToken(AccessToken $token)
{
$this->accessToken = $token;
if (is_callable($this->tokenSave)) {
call_user_func($this->tokenSave, $this->accessToken);
}
}
}

0 comments on commit 2c939a7

Please sign in to comment.