22
33namespace Platformsh \OAuth2 \Client ;
44
5+ use GuzzleHttp \Exception \BadResponseException ;
56use League \OAuth2 \Client \Grant \AbstractGrant ;
67use League \OAuth2 \Client \Grant \ClientCredentials ;
78use League \OAuth2 \Client \Grant \RefreshToken ;
89use League \OAuth2 \Client \Provider \AbstractProvider ;
10+ use League \OAuth2 \Client \Provider \Exception \IdentityProviderException ;
911use League \OAuth2 \Client \Token \AccessToken ;
1012use Psr \Http \Message \RequestInterface ;
1113use 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