Skip to content

Commit e659251

Browse files
committed
Add hooks for the refresh cycle and step-up authentication
1 parent 963fe99 commit e659251

File tree

1 file changed

+140
-23
lines changed

1 file changed

+140
-23
lines changed

src/GuzzleMiddleware.php

Lines changed: 140 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace Platformsh\OAuth2\Client;
44

5+
use GuzzleHttp\Exception\BadResponseException;
56
use League\OAuth2\Client\Grant\AbstractGrant;
67
use League\OAuth2\Client\Grant\ClientCredentials;
78
use League\OAuth2\Client\Grant\RefreshToken;
89
use League\OAuth2\Client\Provider\AbstractProvider;
10+
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
911
use League\OAuth2\Client\Token\AccessToken;
1012
use Psr\Http\Message\RequestInterface;
1113
use Psr\Http\Message\ResponseInterface;
@@ -18,20 +20,32 @@ class GuzzleMiddleware
1820
/** @var AbstractGrant $grant */
1921
private $grant;
2022

21-
/** @var \League\OAuth2\Client\Token\AccessToken|null */
23+
/** @var AccessToken|null */
2224
private $accessToken;
2325

2426
/** @var array */
25-
private $grantOptions = [];
27+
private $grantOptions;
2628

2729
/** @var callable|null */
2830
private $tokenSave;
2931

32+
/** @var callable|null */
33+
protected $onRefreshStart;
34+
35+
/** @var callable|null */
36+
protected $onRefreshEnd;
37+
38+
/** @var callable|null */
39+
protected $onRefreshError;
40+
41+
/** @var callable|null */
42+
protected $onStepUpAuthResponse;
43+
3044
/**
3145
* GuzzleMiddleware constructor.
3246
*
33-
* @param \League\OAuth2\Client\Provider\AbstractProvider $provider
34-
* @param \League\OAuth2\Client\Grant\AbstractGrant $grant
47+
* @param AbstractProvider $provider
48+
* @param AbstractGrant $grant
3549
* @param array $grantOptions
3650
*/
3751
public function __construct(AbstractProvider $provider, AbstractGrant $grant = null, array $grantOptions = [])
@@ -53,6 +67,54 @@ public function setTokenSaveCallback(callable $tokenSave)
5367
$this->tokenSave = $tokenSave;
5468
}
5569

70+
/**
71+
* Sets a callback that will be triggered when token refresh starts.
72+
*
73+
* @param callable $callback
74+
* A callback which accepts 1 argument, the refresh token being used if
75+
* available (a string or null), and returns an AccessToken or null.
76+
*/
77+
public function setOnRefreshStart(callable $callback)
78+
{
79+
$this->onRefreshStart = $callback;
80+
}
81+
82+
/**
83+
* Set a callback that will be triggered when token refresh ends.
84+
*
85+
* @param callable $callback
86+
* A callback which accepts 1 argument, the refresh token which was used
87+
* if available (a string or null).
88+
*/
89+
public function setOnRefreshEnd(callable $callback)
90+
{
91+
$this->onRefreshEnd = $callback;
92+
}
93+
94+
/**
95+
* Set a callback that will react to a refresh token error.
96+
*
97+
* @param callable $callback
98+
* A callback which accepts one argument, the BadResponseException, and
99+
* returns an AccessToken or null.
100+
*/
101+
public function setOnRefreshError(callable $callback)
102+
{
103+
$this->onRefreshError = $callback;
104+
}
105+
106+
/**
107+
* Set a callback that will react to a step-up authentication response (RFC 9470).
108+
*
109+
* @param callable $callback
110+
* A callback which accepts one argument, the response, of type \GuzzleHttp\Message\ResponseInterface,
111+
* and returns an AccessToken or null.
112+
*/
113+
public function setOnStepUpAuthResponse(callable $callback)
114+
{
115+
$this->onStepUpAuthResponse = $callback;
116+
}
117+
56118
/**
57119
* Main middleware callback.
58120
*
@@ -73,23 +135,42 @@ public function __invoke(callable $next)
73135
/** @var \GuzzleHttp\Promise\PromiseInterface $promise */
74136
$promise = $next($request, $options);
75137

76-
return $promise->then(
77-
function (ResponseInterface $response) use ($request, $options, $token, $next) {
78-
if ($response->getStatusCode() === 401) {
79-
// Consider the old token invalid, and get a new one.
80-
$token = $this->getAccessToken($token);
138+
return $promise->then(function (ResponseInterface $response) use ($request, $options, $token, $next) {
139+
if ($response->getStatusCode() !== 401) {
140+
return $response;
141+
}
81142

82-
// Retry the request.
83-
$request = $this->authenticateRequest($request, $token);
84-
$response = $next($request, $options);
143+
if (isset($this->onStepUpAuthResponse) && $this->isStepUpAuthenticationResponse($response)) {
144+
$newToken = call_user_func($this->onStepUpAuthResponse, $response);
145+
$this->accessToken = $newToken;
146+
if (is_callable($this->tokenSave)) {
147+
call_user_func($this->tokenSave, $this->accessToken);
85148
}
86-
87-
return $response;
149+
} else {
150+
// Consider the old token invalid, and get a new one.
151+
$this->getAccessToken($token);
88152
}
89-
);
153+
154+
// Retry the request.
155+
$request = $this->authenticateRequest($request, $token);
156+
return $next($request, $options);
157+
});
90158
};
91159
}
92160

161+
/**
162+
* Checks for a step-up authentication response (RFC 9470).
163+
*
164+
* @param ResponseInterface $response
165+
*
166+
* @return bool
167+
*/
168+
protected function isStepUpAuthenticationResponse(ResponseInterface $response)
169+
{
170+
$authHeader = implode("\n", $response->getHeader('WWW-Authenticate'));
171+
return stripos($authHeader, 'Bearer') !== false && strpos($authHeader, 'insufficient_user_authentication') !== false;
172+
}
173+
93174
/**
94175
* Check if a request is configured to use OAuth2.
95176
*
@@ -116,10 +197,10 @@ private function isOAuth2(RequestInterface $request, array $options)
116197
/**
117198
* Add authentication to an HTTP request.
118199
*
119-
* @param \Psr\Http\Message\RequestInterface $request
120-
* @param \League\OAuth2\Client\Token\AccessToken $token
200+
* @param RequestInterface $request
201+
* @param AccessToken $token
121202
*
122-
* @return \Psr\Http\Message\RequestInterface
203+
* @return RequestInterface
123204
*/
124205
private function authenticateRequest(RequestInterface $request, AccessToken $token)
125206
{
@@ -131,13 +212,14 @@ private function authenticateRequest(RequestInterface $request, AccessToken $tok
131212
}
132213

133214
/**
134-
* Get the current access token.
215+
* Get the current or a new access token.
135216
*
136217
* @param AccessToken|null $invalid
137218
* A token to consider invalid.
138219
*
139-
* @return \League\OAuth2\Client\Token\AccessToken
220+
* @return AccessToken
140221
* The OAuth2 access token.
222+
* @throws IdentityProviderException
141223
*/
142224
private function getAccessToken(AccessToken $invalid = null)
143225
{
@@ -155,23 +237,58 @@ private function getAccessToken(AccessToken $invalid = null)
155237
* Acquire a new access token using a refresh token or the configured grant.
156238
*
157239
* @return AccessToken
240+
* @throws IdentityProviderException
158241
*/
159242
private function acquireAccessToken()
160243
{
161244
if (isset($this->accessToken) && $this->accessToken->getRefreshToken()) {
162-
return $this->provider->getAccessToken(new RefreshToken(), ['refresh_token' => $this->accessToken->getRefreshToken()]);
245+
$currentRefreshToken = $this->accessToken->getRefreshToken();
246+
try {
247+
if (isset($this->onRefreshStart)) {
248+
$result = call_user_func($this->onRefreshStart, $currentRefreshToken);
249+
if ($result instanceof AccessToken) {
250+
return $result;
251+
}
252+
}
253+
return $this->provider->getAccessToken(new RefreshToken(), ['refresh_token' => $this->accessToken->getRefreshToken()]);
254+
} catch (BadResponseException $e) {
255+
if (isset($this->onRefreshError)) {
256+
$accessToken = call_user_func($this->onRefreshError, $e);
257+
if ($accessToken) {
258+
return $accessToken;
259+
}
260+
}
261+
throw $e;
262+
} finally {
263+
if (isset($this->onRefreshEnd)) {
264+
call_user_func($this->onRefreshEnd, $currentRefreshToken);
265+
}
266+
}
163267
}
164268

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

168272
/**
169-
* Set the access token for the next request.
273+
* Set the access token for the next request(s).
170274
*
171-
* @param \League\OAuth2\Client\Token\AccessToken $token
275+
* @param AccessToken $token
172276
*/
173277
public function setAccessToken(AccessToken $token)
174278
{
175279
$this->accessToken = $token;
176280
}
281+
282+
/**
283+
* Set the access token for the next request(s), and save it to storage.
284+
*
285+
* @param AccessToken $token
286+
*/
287+
public function saveAccessToken(AccessToken $token)
288+
{
289+
$this->accessToken = $token;
290+
if (is_callable($this->tokenSave)) {
291+
call_user_func($this->tokenSave, $this->accessToken);
292+
}
293+
}
177294
}

0 commit comments

Comments
 (0)