100.00% covered (success)
@@ -398,660 +398,668 @@
186
187
188
-
189
-
190
-
191
-
192
-
193 public function __construct ( array $config ) {
-
194 $config = array_merge ( [
-
195 'csrfMiddleware' => new Middleware \ DoubleSubmitCookieCsrfMiddleware ( self :: DEFAULT_CSRF_KEY ) ,
-
196 'logger' => null ,
-
197 self :: HANDLE_NON_INDIEAUTH_REQUEST => function ( ServerRequestInterface $request ) { return null ; } ,
-
198 'tokenStorage' => null ,
-
199 'httpGetWithEffectiveUrl' => null ,
-
200 'authorizationForm' => new DefaultAuthorizationForm ( ) ,
-
201 'exceptionTemplatePath' => __DIR__ . '/../templates/default_exception_response.html.php' ,
-
202 'requirePKCE' => true ,
-
203 ] , $config ) ;
-
204
-
205 $this -> requirePkce = $config [ 'requirePKCE' ] ;
-
206
-
207 if ( ! is_string ( $config [ 'exceptionTemplatePath' ] ) ) {
-
208 throw new BadMethodCallException ( "\$config['exceptionTemplatePath'] must be a string (path)." ) ;
-
209 }
-
210 $this -> exceptionTemplatePath = $config [ 'exceptionTemplatePath' ] ;
-
211
-
212 $secret = $config [ 'secret' ] ?? '' ;
-
213 if ( ! is_string ( $secret ) || strlen ( $secret ) < 64 ) {
-
214 throw new BadMethodCallException ( "\$config['secret'] must be a string with a minimum length of 64 characters." ) ;
-
215 }
-
216 $this -> secret = $secret ;
-
217
-
218 if ( ! is_null ( $config [ 'logger' ] ) && ! $config [ 'logger' ] instanceof LoggerInterface ) {
-
219 throw new BadMethodCallException ( "\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null." ) ;
-
220 }
-
221 $this -> logger = $config [ 'logger' ] ?? new NullLogger ( ) ;
-
222
-
223 if ( ! ( array_key_exists ( self :: HANDLE_AUTHENTICATION_REQUEST , $config ) and is_callable ( $config [ self :: HANDLE_AUTHENTICATION_REQUEST ] ) ) ) {
-
224 throw new BadMethodCallException ( '$callbacks[\'' . self :: HANDLE_AUTHENTICATION_REQUEST . '\'] must be present and callable.' ) ;
-
225 }
-
226 $this -> handleAuthenticationRequestCallback = $config [ self :: HANDLE_AUTHENTICATION_REQUEST ] ;
-
227
-
228 if ( ! is_callable ( $config [ self :: HANDLE_NON_INDIEAUTH_REQUEST ] ) ) {
-
229 throw new BadMethodCallException ( "\$config['" . self :: HANDLE_NON_INDIEAUTH_REQUEST . "'] must be callable" ) ;
-
230 }
-
231 $this -> handleNonIndieAuthRequest = $config [ self :: HANDLE_NON_INDIEAUTH_REQUEST ] ;
-
232
-
233 $tokenStorage = $config [ 'tokenStorage' ] ;
-
234 if ( ! $tokenStorage instanceof Storage \ TokenStorageInterface ) {
-
235 if ( is_string ( $tokenStorage ) ) {
-
236
-
237 $tokenStorage = new Storage \ FilesystemJsonStorage ( $tokenStorage , $this -> secret ) ;
-
238 } else {
-
239 throw new BadMethodCallException ( "\$config['tokenStorage'] parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface." ) ;
-
240 }
-
241 }
-
242 trySetLogger ( $tokenStorage , $this -> logger ) ;
-
243 $this -> tokenStorage = $tokenStorage ;
-
244
-
245 $csrfMiddleware = $config [ 'csrfMiddleware' ] ;
-
246 if ( ! $csrfMiddleware instanceof MiddlewareInterface ) {
-
247 throw new BadMethodCallException ( "\$config['csrfMiddleware'] must be null or implement MiddlewareInterface." ) ;
-
248 }
-
249 trySetLogger ( $csrfMiddleware , $this -> logger ) ;
-
250 $this -> csrfMiddleware = $csrfMiddleware ;
-
251
-
252 $httpGetWithEffectiveUrl = $config [ 'httpGetWithEffectiveUrl' ] ;
-
253 if ( is_null ( $httpGetWithEffectiveUrl ) ) {
-
254 if ( class_exists ( '\GuzzleHttp\Client' ) ) {
-
255 $httpGetWithEffectiveUrl = function ( string $uri ) : array {
-
256
-
257
-
258 $resp = ( new \ GuzzleHttp \ Client ( [
-
259 \ GuzzleHttp \ RequestOptions :: ALLOW_REDIRECTS => [
-
260 'max' => 10 ,
-
261 'strict' => true ,
-
262 'referer' => true ,
-
263 'track_redirects' => true
-
264 ]
-
265 ] ) ) -> get ( $uri ) ;
-
266
-
267 $rdh = $resp -> getHeader ( 'X-Guzzle-Redirect-History' ) ;
-
268 $effectiveUrl = empty ( $rdh ) ? $uri : array_values ( $rdh ) [ count ( $rdh ) - 1 ] ;
-
269
-
270 return [ $resp , $effectiveUrl ] ;
-
271 } ;
-
272 } else {
-
273 throw new BadMethodCallException ( "\$config['httpGetWithEffectiveUrl'] was not provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable." ) ;
-
274
-
275 }
-
276 } else {
-
277 if ( ! is_callable ( $httpGetWithEffectiveUrl ) ) {
-
278 throw new BadMethodCallException ( "\$config['httpGetWithEffectiveUrl'] must be callable." ) ;
-
279 }
-
280 }
-
281 trySetLogger ( $httpGetWithEffectiveUrl , $this -> logger ) ;
-
282 $this -> httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl ;
-
283
-
284 if ( ! $config [ 'authorizationForm' ] instanceof AuthorizationFormInterface ) {
-
285 throw new BadMethodCallException ( "When provided, \$config['authorizationForm'] must implement Taproot\IndieAuth\Callback\AuthorizationForm." ) ;
-
286 }
-
287 $this -> authorizationForm = $config [ 'authorizationForm' ] ;
-
288 trySetLogger ( $this -> authorizationForm , $this -> logger ) ;
-
289 }
-
290
-
291 public function getTokenStorage ( ) : TokenStorageInterface {
-
292 return $this -> tokenStorage ;
-
293 }
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330 public function handleAuthorizationEndpointRequest ( ServerRequestInterface $request ) : ResponseInterface {
-
331 $this -> logger -> info ( 'Handling an IndieAuth Authorization Endpoint request.' ) ;
-
332
-
333
-
334 if ( isIndieAuthAuthorizationCodeRedeemingRequest ( $request ) ) {
-
335 $this -> logger -> info ( 'Handling a request to redeem an authorization code for profile information.' ) ;
-
336
-
337 $bodyParams = $request -> getParsedBody ( ) ;
-
338
-
339 if ( ! isset ( $bodyParams [ 'code' ] ) ) {
-
340 $this -> logger -> warning ( 'The exchange request was missing the code parameter. Returning an error response.' ) ;
-
341 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
342 'error' => 'invalid_request' ,
-
343 'error_description' => 'The code parameter was missing.'
-
344 ] ) ) ;
-
345 }
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201 public function __construct ( array $config ) {
+
202 $config = array_merge ( [
+
203 'csrfMiddleware' => new Middleware \ DoubleSubmitCookieCsrfMiddleware ( self :: DEFAULT_CSRF_KEY ) ,
+
204 'logger' => null ,
+
205 self :: HANDLE_NON_INDIEAUTH_REQUEST => function ( ServerRequestInterface $request ) { return null ; } ,
+
206 'tokenStorage' => null ,
+
207 'httpGetWithEffectiveUrl' => null ,
+
208 'authorizationForm' => new DefaultAuthorizationForm ( ) ,
+
209 'exceptionTemplatePath' => __DIR__ . '/../templates/default_exception_response.html.php' ,
+
210 'requirePKCE' => true ,
+
211 ] , $config ) ;
+
212
+
213 $this -> requirePkce = $config [ 'requirePKCE' ] ;
+
214
+
215 if ( ! is_string ( $config [ 'exceptionTemplatePath' ] ) ) {
+
216 throw new BadMethodCallException ( "\$config['exceptionTemplatePath'] must be a string (path)." ) ;
+
217 }
+
218 $this -> exceptionTemplatePath = $config [ 'exceptionTemplatePath' ] ;
+
219
+
220 $secret = $config [ 'secret' ] ?? '' ;
+
221 if ( ! is_string ( $secret ) || strlen ( $secret ) < 64 ) {
+
222 throw new BadMethodCallException ( "\$config['secret'] must be a string with a minimum length of 64 characters." ) ;
+
223 }
+
224 $this -> secret = $secret ;
+
225
+
226 if ( ! is_null ( $config [ 'logger' ] ) && ! $config [ 'logger' ] instanceof LoggerInterface ) {
+
227 throw new BadMethodCallException ( "\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null." ) ;
+
228 }
+
229 $this -> logger = $config [ 'logger' ] ?? new NullLogger ( ) ;
+
230
+
231 if ( ! ( array_key_exists ( self :: HANDLE_AUTHENTICATION_REQUEST , $config ) and is_callable ( $config [ self :: HANDLE_AUTHENTICATION_REQUEST ] ) ) ) {
+
232 throw new BadMethodCallException ( '$callbacks[\'' . self :: HANDLE_AUTHENTICATION_REQUEST . '\'] must be present and callable.' ) ;
+
233 }
+
234 $this -> handleAuthenticationRequestCallback = $config [ self :: HANDLE_AUTHENTICATION_REQUEST ] ;
+
235
+
236 if ( ! is_callable ( $config [ self :: HANDLE_NON_INDIEAUTH_REQUEST ] ) ) {
+
237 throw new BadMethodCallException ( "\$config['" . self :: HANDLE_NON_INDIEAUTH_REQUEST . "'] must be callable" ) ;
+
238 }
+
239 $this -> handleNonIndieAuthRequest = $config [ self :: HANDLE_NON_INDIEAUTH_REQUEST ] ;
+
240
+
241 $tokenStorage = $config [ 'tokenStorage' ] ;
+
242 if ( ! $tokenStorage instanceof Storage \ TokenStorageInterface ) {
+
243 if ( is_string ( $tokenStorage ) ) {
+
244
+
245 $tokenStorage = new Storage \ FilesystemJsonStorage ( $tokenStorage , $this -> secret ) ;
+
246 } else {
+
247 throw new BadMethodCallException ( "\$config['tokenStorage'] parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface." ) ;
+
248 }
+
249 }
+
250 trySetLogger ( $tokenStorage , $this -> logger ) ;
+
251 $this -> tokenStorage = $tokenStorage ;
+
252
+
253 $csrfMiddleware = $config [ 'csrfMiddleware' ] ;
+
254 if ( ! $csrfMiddleware instanceof MiddlewareInterface ) {
+
255 throw new BadMethodCallException ( "\$config['csrfMiddleware'] must be null or implement MiddlewareInterface." ) ;
+
256 }
+
257 trySetLogger ( $csrfMiddleware , $this -> logger ) ;
+
258 $this -> csrfMiddleware = $csrfMiddleware ;
+
259
+
260 $httpGetWithEffectiveUrl = $config [ 'httpGetWithEffectiveUrl' ] ;
+
261 if ( is_null ( $httpGetWithEffectiveUrl ) ) {
+
262 if ( class_exists ( '\GuzzleHttp\Client' ) ) {
+
263 $httpGetWithEffectiveUrl = function ( string $uri ) : array {
+
264
+
265
+
266 $resp = ( new \ GuzzleHttp \ Client ( [
+
267 \ GuzzleHttp \ RequestOptions :: ALLOW_REDIRECTS => [
+
268 'max' => 10 ,
+
269 'strict' => true ,
+
270 'referer' => true ,
+
271 'track_redirects' => true
+
272 ]
+
273 ] ) ) -> get ( $uri ) ;
+
274
+
275 $rdh = $resp -> getHeader ( 'X-Guzzle-Redirect-History' ) ;
+
276 $effectiveUrl = empty ( $rdh ) ? $uri : array_values ( $rdh ) [ count ( $rdh ) - 1 ] ;
+
277
+
278 return [ $resp , $effectiveUrl ] ;
+
279 } ;
+
280 } else {
+
281 throw new BadMethodCallException ( "\$config['httpGetWithEffectiveUrl'] was not provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable." ) ;
+
282
+
283 }
+
284 } else {
+
285 if ( ! is_callable ( $httpGetWithEffectiveUrl ) ) {
+
286 throw new BadMethodCallException ( "\$config['httpGetWithEffectiveUrl'] must be callable." ) ;
+
287 }
+
288 }
+
289 trySetLogger ( $httpGetWithEffectiveUrl , $this -> logger ) ;
+
290 $this -> httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl ;
+
291
+
292 if ( ! $config [ 'authorizationForm' ] instanceof AuthorizationFormInterface ) {
+
293 throw new BadMethodCallException ( "When provided, \$config['authorizationForm'] must implement Taproot\IndieAuth\Callback\AuthorizationForm." ) ;
+
294 }
+
295 $this -> authorizationForm = $config [ 'authorizationForm' ] ;
+
296 trySetLogger ( $this -> authorizationForm , $this -> logger ) ;
+
297 }
+
298
+
299 public function getTokenStorage ( ) : TokenStorageInterface {
+
300 return $this -> tokenStorage ;
+
301 }
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+
314
+
315
+
316
+
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338 public function handleAuthorizationEndpointRequest ( ServerRequestInterface $request ) : ResponseInterface {
+
339 $this -> logger -> info ( 'Handling an IndieAuth Authorization Endpoint request.' ) ;
+
340
+
341
+
342 if ( isIndieAuthAuthorizationCodeRedeemingRequest ( $request ) ) {
+
343 $this -> logger -> info ( 'Handling a request to redeem an authorization code for profile information.' ) ;
+
344
+
345 $bodyParams = $request -> getParsedBody ( ) ;
346
-
347
-
348
-
349
-
350
-
351 try {
-
352
-
353
-
354 $tokenData = $this -> tokenStorage -> exchangeAuthCodeForAccessToken ( $bodyParams [ 'code' ] , function ( array $authCode ) use ( $request , $bodyParams ) {
-
355
-
356 $requiredParameters = ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) ? [ 'client_id' , 'redirect_uri' , 'code_verifier' ] : [ 'client_id' , 'redirect_uri' ] ;
-
357 $missingRequiredParameters = array_filter ( $requiredParameters , function ( $p ) use ( $bodyParams ) {
-
358 return ! array_key_exists ( $p , $bodyParams ) || empty ( $bodyParams [ $p ] ) ;
-
359 } ) ;
-
360 if ( ! empty ( $missingRequiredParameters ) ) {
-
361 $this -> logger -> warning ( 'The exchange request was missing required parameters. Returning an error response.' , [ 'missing' => $missingRequiredParameters ] ) ;
-
362 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST , $request ) ;
-
363 }
-
364
-
365
-
366 if ( $authCode [ 'client_id' ] !== $bodyParams [ 'client_id' ]
-
367 || $authCode [ 'redirect_uri' ] !== $bodyParams [ 'redirect_uri' ] ) {
-
368 $this -> logger -> error ( "The provided client_id and/or redirect_uri did not match those stored in the token." ) ;
-
369 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
370 }
-
371
-
372
-
373
-
374 if ( ! empty ( $bodyParams [ 'code_verifier' ] ) && empty ( $authCode [ 'code_challenge' ] ) ) {
-
375 $this -> logger -> error ( "A code_verifier was provided when trying to exchange an auth code requested without a code_challenge." ) ;
-
376 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
377 }
-
378
-
379 if ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) {
-
380
-
381
-
382 if ( ! hash_equals ( $authCode [ 'code_challenge' ] , generatePKCECodeChallenge ( $bodyParams [ 'code_verifier' ] ) ) ) {
-
383 $this -> logger -> error ( "The provided code_verifier did not hash to the stored code_challenge" ) ;
-
384 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
385 }
-
386 }
-
387
-
388
-
389 $requestedScopes = array_filter ( explode ( ' ' , $authCode [ 'scope' ] ?? '' ) ) ;
-
390 if ( ! empty ( $requestedScopes ) && $requestedScopes != [ 'profile' ] ) {
-
391 $this -> logger -> error ( "An exchange request for a token granting scopes other than “profile” was sent to the authorization endpoint." ) ;
-
392 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
393 }
-
394 } ) ;
-
395 } catch ( IndieAuthException $e ) {
-
396
-
397 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
398 'error' => $e -> getInfo ( ) [ 'error' ] ,
-
399 'error_description' => $e -> getMessage ( )
-
400 ] ) ) ;
-
401 }
-
402
-
403 if ( is_null ( $tokenData ) ) {
-
404 $this -> logger -> error ( 'Attempting to exchange an auth code for a token resulted in null.' , $bodyParams ) ;
-
405 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
406 'error' => 'invalid_grant' ,
-
407 'error_description' => 'The provided credentials were not valid.'
+
347 if ( ! isset ( $bodyParams [ 'code' ] ) ) {
+
348 $this -> logger -> warning ( 'The exchange request was missing the code parameter. Returning an error response.' ) ;
+
349 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
350 'error' => 'invalid_request' ,
+
351 'error_description' => 'The code parameter was missing.'
+
352 ] ) ) ;
+
353 }
+
354
+
355
+
356
+
357
+
358
+
359 try {
+
360
+
361
+
362 $tokenData = $this -> tokenStorage -> exchangeAuthCodeForAccessToken ( $bodyParams [ 'code' ] , function ( array $authCode ) use ( $request , $bodyParams ) {
+
363
+
364 $requiredParameters = ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) ? [ 'client_id' , 'redirect_uri' , 'code_verifier' ] : [ 'client_id' , 'redirect_uri' ] ;
+
365 $missingRequiredParameters = array_filter ( $requiredParameters , function ( $p ) use ( $bodyParams ) {
+
366 return ! array_key_exists ( $p , $bodyParams ) || empty ( $bodyParams [ $p ] ) ;
+
367 } ) ;
+
368 if ( ! empty ( $missingRequiredParameters ) ) {
+
369 $this -> logger -> warning ( 'The exchange request was missing required parameters. Returning an error response.' , [ 'missing' => $missingRequiredParameters ] ) ;
+
370 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST , $request ) ;
+
371 }
+
372
+
373
+
374 if ( $authCode [ 'client_id' ] !== $bodyParams [ 'client_id' ]
+
375 || $authCode [ 'redirect_uri' ] !== $bodyParams [ 'redirect_uri' ] ) {
+
376 $this -> logger -> error ( "The provided client_id and/or redirect_uri did not match those stored in the token." ) ;
+
377 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
378 }
+
379
+
380
+
381
+
382 if ( ! empty ( $bodyParams [ 'code_verifier' ] ) && empty ( $authCode [ 'code_challenge' ] ) ) {
+
383 $this -> logger -> error ( "A code_verifier was provided when trying to exchange an auth code requested without a code_challenge." ) ;
+
384 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
385 }
+
386
+
387 if ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) {
+
388
+
389
+
390 if ( ! hash_equals ( $authCode [ 'code_challenge' ] , generatePKCECodeChallenge ( $bodyParams [ 'code_verifier' ] ) ) ) {
+
391 $this -> logger -> error ( "The provided code_verifier did not hash to the stored code_challenge" ) ;
+
392 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
393 }
+
394 }
+
395
+
396
+
397 $requestedScopes = array_filter ( explode ( ' ' , $authCode [ 'scope' ] ?? '' ) ) ;
+
398 if ( ! empty ( $requestedScopes ) && $requestedScopes != [ 'profile' ] ) {
+
399 $this -> logger -> error ( "An exchange request for a token granting scopes other than “profile” was sent to the authorization endpoint." ) ;
+
400 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
401 }
+
402 } ) ;
+
403 } catch ( IndieAuthException $e ) {
+
404
+
405 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
406 'error' => $e -> getInfo ( ) [ 'error' ] ,
+
407 'error_description' => $e -> getMessage ( )
408 ] ) ) ;
409 }
410
-
411
-
412
-
413
-
414 return new Response ( 200 , [
-
415 'content-type' => 'application/json' ,
-
416 'cache-control' => 'no-store' ,
-
417 ] , json_encode ( array_filter ( $tokenData , function ( string $k ) {
-
418
-
419
-
420 return in_array ( $k , [ 'me' , 'profile' ] ) ;
-
421 } , ARRAY_FILTER_USE_KEY ) ) ) ;
-
422 }
-
423
-
424
-
425
-
426 return $this -> csrfMiddleware -> process ( $request , new Middleware \ ClosureRequestHandler ( function ( ServerRequestInterface $request ) {
-
427
-
428
-
429
-
430 try {
-
431 $queryParams = $request -> getQueryParams ( ) ;
-
432
-
433
-
434
-
435
-
436 list ( $clientIdResponse , $clientIdEffectiveUrl , $clientIdMf2 ) = [ null , null , null ] ;
-
437
-
438
-
439
-
440 if ( isIndieAuthAuthorizationRequest ( $request , [ 'get' , 'post' ] ) ) {
-
441 $this -> logger -> info ( 'Handling an authorization request' , [ 'method' => $request -> getMethod ( ) ] ) ;
-
442
-
443
-
444 if ( ! isset ( $queryParams [ 'client_id' ] ) || false === filter_var ( $queryParams [ 'client_id' ] , FILTER_VALIDATE_URL ) || ! isClientIdentifier ( $queryParams [ 'client_id' ] ) ) {
-
445 $this -> logger -> warning ( "The client_id provided in an authorization request was not valid." , $queryParams ) ;
-
446 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CLIENT_ID , $request ) ;
-
447 }
-
448
-
449
-
450 if ( ! isset ( $queryParams [ 'redirect_uri' ] ) || false === filter_var ( $queryParams [ 'redirect_uri' ] , FILTER_VALIDATE_URL ) ) {
-
451 $this -> logger -> warning ( "The client_id provided in an authorization request was not valid." , $queryParams ) ;
-
452 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REDIRECT_URI , $request ) ;
-
453 }
-
454
-
455
-
456
-
457
-
458 $currentRequestHash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
-
459 if ( ! isset ( $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) or is_null ( $currentRequestHash ) or ! hash_equals ( $currentRequestHash , $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) ) {
-
460
-
461
-
462
-
463 if ( ! urlComponentsMatch ( $queryParams [ 'client_id' ] , $queryParams [ 'redirect_uri' ] , [ PHP_URL_SCHEME , PHP_URL_HOST , PHP_URL_PORT ] ) ) {
-
464
-
465
-
466
-
467 try {
-
468 list ( $clientIdResponse , $clientIdEffectiveUrl ) = call_user_func ( $this -> httpGetWithEffectiveUrl , $queryParams [ 'client_id' ] ) ;
-
469 $clientIdMf2 = Mf2 \ parse ( (string) $clientIdResponse -> getBody ( ) , $clientIdEffectiveUrl ) ;
-
470 } catch ( ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e ) {
-
471 $this -> logger -> error ( "Caught an HTTP exception while trying to fetch the client_id. Returning an error response." , [
-
472 'client_id' => $queryParams [ 'client_id' ] ,
-
473 'exception' => $e -> __toString ( )
-
474 ] ) ;
-
475
-
476 throw IndieAuthException :: create ( IndieAuthException :: HTTP_EXCEPTION_FETCHING_CLIENT_ID , $request , $e ) ;
-
477 } catch ( Exception $e ) {
-
478 $this -> logger -> error ( "Caught an unknown exception while trying to fetch the client_id. Returning an error response." , [
-
479 'exception' => $e -> __toString ( )
-
480 ] ) ;
-
481
-
482 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_EXCEPTION_FETCHING_CLIENT_ID , $request , $e ) ;
-
483 }
-
484
-
485
-
486 $clientIdRedirectUris = [ ] ;
-
487 if ( array_key_exists ( 'redirect_uri' , $clientIdMf2 [ 'rels' ] ) ) {
-
488 $clientIdRedirectUris = array_merge ( $clientIdRedirectUris , $clientIdMf2 [ 'rels' ] [ 'redirect_uri' ] ) ;
-
489 }
-
490
-
491 foreach ( HeaderParser :: parse ( $clientIdResponse -> getHeader ( 'Link' ) ) as $link ) {
-
492 if ( array_key_exists ( 'rel' , $link ) && mb_strpos ( " { $link [ 'rel' ] } " , " redirect_uri " ) !== false ) {
-
493
-
494 $clientIdRedirectUris [ ] = substr ( $link [ 0 ] , 1 , strlen ( $link [ 0 ] ) - 2 ) ;
-
495 }
-
496 }
-
497
-
498
-
499 if ( ! in_array ( $queryParams [ 'redirect_uri' ] , $clientIdRedirectUris ) ) {
-
500 $this -> logger -> warning ( "The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs." , [
-
501 'provided_redirect_uri' => $queryParams [ 'redirect_uri' ] ,
-
502 'provided_client_id' => $queryParams [ 'client_id' ] ,
-
503 'discovered_redirect_uris' => $clientIdRedirectUris
-
504 ] ) ;
+
411 if ( is_null ( $tokenData ) ) {
+
412 $this -> logger -> error ( 'Attempting to exchange an auth code for a token resulted in null.' , $bodyParams ) ;
+
413 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
414 'error' => 'invalid_grant' ,
+
415 'error_description' => 'The provided credentials were not valid.'
+
416 ] ) ) ;
+
417 }
+
418
+
419
+
420
+
421
+
422 return new Response ( 200 , [
+
423 'content-type' => 'application/json' ,
+
424 'cache-control' => 'no-store' ,
+
425 ] , json_encode ( array_filter ( $tokenData , function ( string $k ) {
+
426
+
427
+
428 return in_array ( $k , [ 'me' , 'profile' ] ) ;
+
429 } , ARRAY_FILTER_USE_KEY ) ) ) ;
+
430 }
+
431
+
432
+
433
+
434 return $this -> csrfMiddleware -> process ( $request , new Middleware \ ClosureRequestHandler ( function ( ServerRequestInterface $request ) {
+
435
+
436
+
437
+
438 try {
+
439 $queryParams = $request -> getQueryParams ( ) ;
+
440
+
441
+
442
+
443
+
444 list ( $clientIdResponse , $clientIdEffectiveUrl , $clientIdMf2 ) = [ null , null , null ] ;
+
445
+
446
+
447
+
448 if ( isIndieAuthAuthorizationRequest ( $request , [ 'get' , 'post' ] ) ) {
+
449 $this -> logger -> info ( 'Handling an authorization request' , [ 'method' => $request -> getMethod ( ) ] ) ;
+
450
+
451
+
452 if ( ! isset ( $queryParams [ 'client_id' ] ) || false === filter_var ( $queryParams [ 'client_id' ] , FILTER_VALIDATE_URL ) || ! isClientIdentifier ( $queryParams [ 'client_id' ] ) ) {
+
453 $this -> logger -> warning ( "The client_id provided in an authorization request was not valid." , $queryParams ) ;
+
454 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CLIENT_ID , $request ) ;
+
455 }
+
456
+
457
+
458 if ( ! isset ( $queryParams [ 'redirect_uri' ] ) || false === filter_var ( $queryParams [ 'redirect_uri' ] , FILTER_VALIDATE_URL ) ) {
+
459 $this -> logger -> warning ( "The client_id provided in an authorization request was not valid." , $queryParams ) ;
+
460 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REDIRECT_URI , $request ) ;
+
461 }
+
462
+
463
+
464
+
465
+
466 $currentRequestHash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
+
467 if ( ! isset ( $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) or is_null ( $currentRequestHash ) or ! hash_equals ( $currentRequestHash , $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) ) {
+
468
+
469
+
470
+
471 if ( ! urlComponentsMatch ( $queryParams [ 'client_id' ] , $queryParams [ 'redirect_uri' ] , [ PHP_URL_SCHEME , PHP_URL_HOST , PHP_URL_PORT ] ) ) {
+
472
+
473
+
474
+
475 try {
+
476 list ( $clientIdResponse , $clientIdEffectiveUrl ) = call_user_func ( $this -> httpGetWithEffectiveUrl , $queryParams [ 'client_id' ] ) ;
+
477 $clientIdMf2 = Mf2 \ parse ( (string) $clientIdResponse -> getBody ( ) , $clientIdEffectiveUrl ) ;
+
478 } catch ( ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e ) {
+
479 $this -> logger -> error ( "Caught an HTTP exception while trying to fetch the client_id. Returning an error response." , [
+
480 'client_id' => $queryParams [ 'client_id' ] ,
+
481 'exception' => $e -> __toString ( )
+
482 ] ) ;
+
483
+
484 throw IndieAuthException :: create ( IndieAuthException :: HTTP_EXCEPTION_FETCHING_CLIENT_ID , $request , $e ) ;
+
485 } catch ( Exception $e ) {
+
486 $this -> logger -> error ( "Caught an unknown exception while trying to fetch the client_id. Returning an error response." , [
+
487 'exception' => $e -> __toString ( )
+
488 ] ) ;
+
489
+
490 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_EXCEPTION_FETCHING_CLIENT_ID , $request , $e ) ;
+
491 }
+
492
+
493
+
494 $clientIdRedirectUris = [ ] ;
+
495 if ( array_key_exists ( 'redirect_uri' , $clientIdMf2 [ 'rels' ] ) ) {
+
496 $clientIdRedirectUris = array_merge ( $clientIdRedirectUris , $clientIdMf2 [ 'rels' ] [ 'redirect_uri' ] ) ;
+
497 }
+
498
+
499 foreach ( HeaderParser :: parse ( $clientIdResponse -> getHeader ( 'Link' ) ) as $link ) {
+
500 if ( array_key_exists ( 'rel' , $link ) && mb_strpos ( " { $link [ 'rel' ] } " , " redirect_uri " ) !== false ) {
+
501
+
502 $clientIdRedirectUris [ ] = substr ( $link [ 0 ] , 1 , strlen ( $link [ 0 ] ) - 2 ) ;
+
503 }
+
504 }
505
-
506 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REDIRECT_URI , $request ) ;
-
507 }
-
508 }
-
509 }
-
510
-
511
-
512
+
506
+
507 if ( ! in_array ( $queryParams [ 'redirect_uri' ] , $clientIdRedirectUris ) ) {
+
508 $this -> logger -> warning ( "The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs." , [
+
509 'provided_redirect_uri' => $queryParams [ 'redirect_uri' ] ,
+
510 'provided_client_id' => $queryParams [ 'client_id' ] ,
+
511 'discovered_redirect_uris' => $clientIdRedirectUris
+
512 ] ) ;
513
-
514
-
515 if ( ! isset ( $queryParams [ 'state' ] ) or ! isValidState ( $queryParams [ 'state' ] ) ) {
-
516 $this -> logger -> warning ( "The state provided in an authorization request was not valid." , $queryParams ) ;
-
517 throw IndieAuthException :: create ( IndieAuthException :: INVALID_STATE , $request ) ;
-
518 }
-
519
-
520
-
521
-
522
-
523
-
524 if ( isset ( $queryParams [ 'code_challenge' ] ) or isset ( $queryParams [ 'code_challenge_method' ] ) ) {
-
525 if ( ! isset ( $queryParams [ 'code_challenge' ] ) or ! isValidCodeChallenge ( $queryParams [ 'code_challenge' ] ) ) {
-
526 $this -> logger -> warning ( "The code_challenge provided in an authorization request was not valid." , $queryParams ) ;
-
527 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CODE_CHALLENGE , $request ) ;
-
528 }
-
529
-
530 if ( ! isset ( $queryParams [ 'code_challenge_method' ] ) or ! in_array ( $queryParams [ 'code_challenge_method' ] , [ 'S256' , 'plain' ] ) ) {
-
531 $this -> logger -> error ( "The code_challenge_method parameter was missing or invalid." , $queryParams ) ;
-
532 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CODE_CHALLENGE , $request ) ;
-
533 }
-
534 } else {
-
535
-
536 if ( $this -> requirePkce ) {
-
537 $this -> logger -> warning ( "PKCE is required, and both code_challenge and code_challenge_method were missing." ) ;
-
538 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST_REDIRECT , $request ) ;
-
539 }
-
540 }
-
541
-
542
-
543 if ( array_key_exists ( 'scope' , $queryParams ) && ! isValidScope ( $queryParams [ 'scope' ] ) ) {
-
544 $this -> logger -> warning ( "The scope provided in an authorization request was not valid." , $queryParams ) ;
-
545 throw IndieAuthException :: create ( IndieAuthException :: INVALID_SCOPE , $request ) ;
-
546 }
-
547
-
548
-
549 if ( array_key_exists ( 'me' , $queryParams ) ) {
-
550 $queryParams [ 'me' ] = IndieAuthClient :: normalizeMeURL ( $queryParams [ 'me' ] ) ;
-
551
-
552 if ( false === $queryParams [ 'me' ] || ! isProfileUrl ( $queryParams [ 'me' ] ) ) {
-
553 $queryParams [ 'me' ] = null ;
-
554 }
-
555 }
-
556
-
557
-
558
-
559
-
560
-
561 $hash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
-
562
-
563 $redirectQueryParams = $queryParams ;
-
564 $redirectQueryParams [ self :: HASH_QUERY_STRING_KEY ] = $hash ;
-
565 $authenticationRedirect = $request -> getUri ( ) -> withQuery ( buildQueryString ( $redirectQueryParams ) ) -> __toString ( ) ;
-
566
-
567
-
568 $this -> logger -> info ( 'Calling handle_authentication_request callback' ) ;
-
569 $authenticationResult = call_user_func ( $this -> handleAuthenticationRequestCallback , $request , $authenticationRedirect , $queryParams [ 'me' ] ?? null ) ;
-
570
-
571
-
572 if ( $authenticationResult instanceof ResponseInterface ) {
-
573 return $authenticationResult ;
-
574 } elseif ( is_array ( $authenticationResult ) ) {
-
575
-
576 if ( ! array_key_exists ( 'me' , $authenticationResult ) ) {
-
577 $this -> logger -> error ( 'The handle_authentication_request callback returned an array with no me key.' , [ 'array' => $authenticationResult ] ) ;
-
578 throw IndieAuthException :: create ( IndieAuthException :: AUTHENTICATION_CALLBACK_MISSING_ME_PARAM , $request ) ;
-
579 }
-
580
-
581
-
582 if ( isAuthorizationApprovalRequest ( $request ) ) {
-
583
-
584
-
585
-
586 if ( ! array_key_exists ( self :: HASH_QUERY_STRING_KEY , $queryParams ) ) {
-
587 $this -> logger -> warning ( "An authorization approval request did not have a " . self :: HASH_QUERY_STRING_KEY . " parameter." ) ;
-
588 throw IndieAuthException :: create ( IndieAuthException :: AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH , $request ) ;
-
589 }
-
590
-
591 $expectedHash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
-
592 if ( ! isset ( $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) or is_null ( $expectedHash ) or ! hash_equals ( $expectedHash , $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) ) {
-
593 $this -> logger -> warning ( "The hash provided in the URL was invalid!" , [
-
594 'expected' => $expectedHash ,
-
595 'actual' => $queryParams [ self :: HASH_QUERY_STRING_KEY ]
-
596 ] ) ;
-
597 throw IndieAuthException :: create ( IndieAuthException :: AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH , $request ) ;
-
598 }
-
599
-
600
-
601 $code = array_merge ( $authenticationResult , [
-
602 'client_id' => $queryParams [ 'client_id' ] ,
-
603 'redirect_uri' => $queryParams [ 'redirect_uri' ] ,
-
604 'state' => $queryParams [ 'state' ] ,
-
605 'code_challenge' => $queryParams [ 'code_challenge' ] ?? null ,
-
606 'code_challenge_method' => $queryParams [ 'code_challenge_method' ] ?? null ,
-
607 'requested_scope' => $queryParams [ 'scope' ] ?? '' ,
-
608 ] ) ;
-
609
-
610
-
611 $code = $this -> authorizationForm -> transformAuthorizationCode ( $request , $code ) ;
-
612
-
613
-
614 $authCode = $this -> tokenStorage -> createAuthCode ( $code ) ;
-
615 if ( is_null ( $authCode ) ) {
-
616
-
617
-
618 $this -> logger -> error ( "Saving the authorization code failed and returned false without raising an exception." ) ;
-
619 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request ) ;
-
620 }
-
621
-
622
-
623 return new Response ( 302 , [
-
624 'Location' => appendQueryParams ( $queryParams [ 'redirect_uri' ] , [
-
625 'code' => $authCode ,
-
626 'state' => $code [ 'state' ]
-
627 ] ) ,
-
628 'Cache-control' => 'no-cache'
-
629 ] ) ;
-
630 }
-
631
-
632
-
633
-
634
-
635
-
636
-
637
-
638
-
639
-
640 if ( is_null ( $clientIdResponse ) || is_null ( $clientIdEffectiveUrl ) || is_null ( $clientIdMf2 ) ) {
-
641 try {
-
642
-
643 list ( $clientIdResponse , $clientIdEffectiveUrl ) = call_user_func ( $this -> httpGetWithEffectiveUrl , $queryParams [ 'client_id' ] ) ;
-
644 $clientIdMf2 = Mf2 \ parse ( (string) $clientIdResponse -> getBody ( ) , $clientIdEffectiveUrl ) ;
-
645 } catch ( ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e ) {
-
646 $this -> logger -> error ( "Caught an HTTP exception while trying to fetch the client_id. Returning an error response." , [
-
647 'client_id' => $queryParams [ 'client_id' ] ,
-
648 'exception' => $e -> __toString ( )
-
649 ] ) ;
-
650
-
651
-
652
-
653 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request , $e ) ;
-
654 } catch ( Exception $e ) {
-
655 $this -> logger -> error ( "Caught an unknown exception while trying to fetch the client_id. Returning an error response." , [
+
514 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REDIRECT_URI , $request ) ;
+
515 }
+
516 }
+
517 }
+
518
+
519
+
520
+
521
+
522
+
523 if ( ! isset ( $queryParams [ 'state' ] ) or ! isValidState ( $queryParams [ 'state' ] ) ) {
+
524 $this -> logger -> warning ( "The state provided in an authorization request was not valid." , $queryParams ) ;
+
525 throw IndieAuthException :: create ( IndieAuthException :: INVALID_STATE , $request ) ;
+
526 }
+
527
+
528
+
529
+
530
+
531
+
532 if ( isset ( $queryParams [ 'code_challenge' ] ) or isset ( $queryParams [ 'code_challenge_method' ] ) ) {
+
533 if ( ! isset ( $queryParams [ 'code_challenge' ] ) or ! isValidCodeChallenge ( $queryParams [ 'code_challenge' ] ) ) {
+
534 $this -> logger -> warning ( "The code_challenge provided in an authorization request was not valid." , $queryParams ) ;
+
535 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CODE_CHALLENGE , $request ) ;
+
536 }
+
537
+
538 if ( ! isset ( $queryParams [ 'code_challenge_method' ] ) or ! in_array ( $queryParams [ 'code_challenge_method' ] , [ 'S256' , 'plain' ] ) ) {
+
539 $this -> logger -> error ( "The code_challenge_method parameter was missing or invalid." , $queryParams ) ;
+
540 throw IndieAuthException :: create ( IndieAuthException :: INVALID_CODE_CHALLENGE , $request ) ;
+
541 }
+
542 } else {
+
543
+
544 if ( $this -> requirePkce ) {
+
545 $this -> logger -> warning ( "PKCE is required, and both code_challenge and code_challenge_method were missing." ) ;
+
546 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST_REDIRECT , $request ) ;
+
547 }
+
548 }
+
549
+
550
+
551 if ( array_key_exists ( 'scope' , $queryParams ) && ! isValidScope ( $queryParams [ 'scope' ] ) ) {
+
552 $this -> logger -> warning ( "The scope provided in an authorization request was not valid." , $queryParams ) ;
+
553 throw IndieAuthException :: create ( IndieAuthException :: INVALID_SCOPE , $request ) ;
+
554 }
+
555
+
556
+
557 if ( array_key_exists ( 'me' , $queryParams ) ) {
+
558 $queryParams [ 'me' ] = IndieAuthClient :: normalizeMeURL ( $queryParams [ 'me' ] ) ;
+
559
+
560 if ( false === $queryParams [ 'me' ] || ! isProfileUrl ( $queryParams [ 'me' ] ) ) {
+
561 $queryParams [ 'me' ] = null ;
+
562 }
+
563 }
+
564
+
565
+
566
+
567
+
568
+
569 $hash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
+
570
+
571 $redirectQueryParams = $queryParams ;
+
572 $redirectQueryParams [ self :: HASH_QUERY_STRING_KEY ] = $hash ;
+
573 $authenticationRedirect = $request -> getUri ( ) -> withQuery ( buildQueryString ( $redirectQueryParams ) ) -> __toString ( ) ;
+
574
+
575
+
576 $this -> logger -> info ( 'Calling handle_authentication_request callback' ) ;
+
577 $authenticationResult = call_user_func ( $this -> handleAuthenticationRequestCallback , $request , $authenticationRedirect , $queryParams [ 'me' ] ?? null ) ;
+
578
+
579
+
580 if ( $authenticationResult instanceof ResponseInterface ) {
+
581 return $authenticationResult ;
+
582 } elseif ( is_array ( $authenticationResult ) ) {
+
583
+
584 if ( ! array_key_exists ( 'me' , $authenticationResult ) ) {
+
585 $this -> logger -> error ( 'The handle_authentication_request callback returned an array with no me key.' , [ 'array' => $authenticationResult ] ) ;
+
586 throw IndieAuthException :: create ( IndieAuthException :: AUTHENTICATION_CALLBACK_MISSING_ME_PARAM , $request ) ;
+
587 }
+
588
+
589
+
590 if ( isAuthorizationApprovalRequest ( $request ) ) {
+
591
+
592
+
593
+
594 if ( ! array_key_exists ( self :: HASH_QUERY_STRING_KEY , $queryParams ) ) {
+
595 $this -> logger -> warning ( "An authorization approval request did not have a " . self :: HASH_QUERY_STRING_KEY . " parameter." ) ;
+
596 throw IndieAuthException :: create ( IndieAuthException :: AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH , $request ) ;
+
597 }
+
598
+
599 $expectedHash = hashAuthorizationRequestParameters ( $request , $this -> secret , null , null , $this -> requirePkce ) ;
+
600 if ( ! isset ( $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) or is_null ( $expectedHash ) or ! hash_equals ( $expectedHash , $queryParams [ self :: HASH_QUERY_STRING_KEY ] ) ) {
+
601 $this -> logger -> warning ( "The hash provided in the URL was invalid!" , [
+
602 'expected' => $expectedHash ,
+
603 'actual' => $queryParams [ self :: HASH_QUERY_STRING_KEY ]
+
604 ] ) ;
+
605 throw IndieAuthException :: create ( IndieAuthException :: AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH , $request ) ;
+
606 }
+
607
+
608
+
609 $code = array_merge ( $authenticationResult , [
+
610 'client_id' => $queryParams [ 'client_id' ] ,
+
611 'redirect_uri' => $queryParams [ 'redirect_uri' ] ,
+
612 'state' => $queryParams [ 'state' ] ,
+
613 'code_challenge' => $queryParams [ 'code_challenge' ] ?? null ,
+
614 'code_challenge_method' => $queryParams [ 'code_challenge_method' ] ?? null ,
+
615 'requested_scope' => $queryParams [ 'scope' ] ?? '' ,
+
616 ] ) ;
+
617
+
618
+
619 $code = $this -> authorizationForm -> transformAuthorizationCode ( $request , $code ) ;
+
620
+
621
+
622 $authCode = $this -> tokenStorage -> createAuthCode ( $code ) ;
+
623 if ( is_null ( $authCode ) ) {
+
624
+
625
+
626 $this -> logger -> error ( "Saving the authorization code failed and returned false without raising an exception." ) ;
+
627 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request ) ;
+
628 }
+
629
+
630
+
631 return new Response ( 302 , [
+
632 'Location' => appendQueryParams ( $queryParams [ 'redirect_uri' ] , [
+
633 'code' => $authCode ,
+
634 'state' => $code [ 'state' ]
+
635 ] ) ,
+
636 'Cache-control' => 'no-cache'
+
637 ] ) ;
+
638 }
+
639
+
640
+
641
+
642
+
643
+
644
+
645
+
646
+
647
+
648 if ( is_null ( $clientIdResponse ) || is_null ( $clientIdEffectiveUrl ) || is_null ( $clientIdMf2 ) ) {
+
649 try {
+
650
+
651 list ( $clientIdResponse , $clientIdEffectiveUrl ) = call_user_func ( $this -> httpGetWithEffectiveUrl , $queryParams [ 'client_id' ] ) ;
+
652 $clientIdMf2 = Mf2 \ parse ( (string) $clientIdResponse -> getBody ( ) , $clientIdEffectiveUrl ) ;
+
653 } catch ( ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e ) {
+
654 $this -> logger -> error ( "Caught an HTTP exception while trying to fetch the client_id. Returning an error response." , [
+
655 'client_id' => $queryParams [ 'client_id' ] ,
656 'exception' => $e -> __toString ( )
657 ] ) ;
-
658
-
659 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request , $e ) ;
-
660 }
-
661 }
-
662
-
663
-
664 $clientHApps = M \ findMicroformatsByProperty ( M \ findMicroformatsByType ( $clientIdMf2 , 'h-app' ) , 'url' , $queryParams [ 'client_id' ] ) ;
-
665 $clientHApp = empty ( $clientHApps ) ? null : $clientHApps [ 0 ] ;
-
666
-
667
-
668 return $this -> authorizationForm -> showForm ( $request , $authenticationResult , $authenticationRedirect , $clientHApp )
-
669 -> withAddedHeader ( 'Cache-control' , 'no-cache' ) ;
-
670 }
-
671 }
-
672
-
673
-
674
-
675 $nonIndieAuthRequestResult = call_user_func ( $this -> handleNonIndieAuthRequest , $request ) ;
-
676 if ( $nonIndieAuthRequestResult instanceof ResponseInterface ) {
-
677 return $nonIndieAuthRequestResult ;
-
678 } else {
-
679
-
680
-
681 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR , $request ) ;
-
682 }
-
683 } catch ( IndieAuthException $e ) {
-
684
-
685 return $this -> handleException ( $e ) ;
-
686 } catch ( Exception $e ) {
-
687
-
688 $this -> logger -> error ( " Caught unknown exception: { $e } " ) ;
-
689 return $this -> handleException ( IndieAuthException :: create ( 0 , $request , $e ) ) ;
-
690 }
-
691 } ) ) ;
-
692 }
-
693
-
694
-
695
-
696
-
697
-
698
-
699
-
700
-
701
-
702
-
703
-
704
-
705
+
658
+
659
+
660
+
661 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request , $e ) ;
+
662 } catch ( Exception $e ) {
+
663 $this -> logger -> error ( "Caught an unknown exception while trying to fetch the client_id. Returning an error response." , [
+
664 'exception' => $e -> __toString ( )
+
665 ] ) ;
+
666
+
667 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR_REDIRECT , $request , $e ) ;
+
668 }
+
669 }
+
670
+
671
+
672 $clientHApps = M \ findMicroformatsByProperty ( M \ findMicroformatsByType ( $clientIdMf2 , 'h-app' ) , 'url' , $queryParams [ 'client_id' ] ) ;
+
673 $clientHApp = empty ( $clientHApps ) ? null : $clientHApps [ 0 ] ;
+
674
+
675
+
676 return $this -> authorizationForm -> showForm ( $request , $authenticationResult , $authenticationRedirect , $clientHApp )
+
677 -> withAddedHeader ( 'Cache-control' , 'no-cache' ) ;
+
678 }
+
679 }
+
680
+
681
+
682
+
683 $nonIndieAuthRequestResult = call_user_func ( $this -> handleNonIndieAuthRequest , $request ) ;
+
684 if ( $nonIndieAuthRequestResult instanceof ResponseInterface ) {
+
685 return $nonIndieAuthRequestResult ;
+
686 } else {
+
687
+
688
+
689 throw IndieAuthException :: create ( IndieAuthException :: INTERNAL_ERROR , $request ) ;
+
690 }
+
691 } catch ( IndieAuthException $e ) {
+
692
+
693 return $this -> handleException ( $e ) ;
+
694 } catch ( Exception $e ) {
+
695
+
696 $this -> logger -> error ( " Caught unknown exception: { $e } " ) ;
+
697 return $this -> handleException ( IndieAuthException :: create ( 0 , $request , $e ) ) ;
+
698 }
+
699 } ) ) ;
+
700 }
+
701
+
702
+
703
+
704
+
705
706
-
707
-
708
-
709
-
710
-
711
-
712 public function handleTokenEndpointRequest ( ServerRequestInterface $request ) : ResponseInterface {
-
713 if ( isIndieAuthAuthorizationCodeRedeemingRequest ( $request ) ) {
-
714 $this -> logger -> info ( 'Handling a request to redeem an authorization code for profile information.' ) ;
-
715
-
716 $bodyParams = $request -> getParsedBody ( ) ;
-
717
-
718 if ( ! isset ( $bodyParams [ 'code' ] ) ) {
-
719 $this -> logger -> warning ( 'The exchange request was missing the code parameter. Returning an error response.' ) ;
-
720 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
721 'error' => 'invalid_request' ,
-
722 'error_description' => 'The code parameter was missing.'
-
723 ] ) ) ;
-
724 }
+
707
+
708
+
709
+
710
+
711
+
712
+
713
+
714
+
715
+
716
+
717
+
718
+
719
+
720 public function handleTokenEndpointRequest ( ServerRequestInterface $request ) : ResponseInterface {
+
721 if ( isIndieAuthAuthorizationCodeRedeemingRequest ( $request ) ) {
+
722 $this -> logger -> info ( 'Handling a request to redeem an authorization code for profile information.' ) ;
+
723
+
724 $bodyParams = $request -> getParsedBody ( ) ;
725
-
726
-
727
-
728
-
729
-
730 try {
-
731
-
732
-
733 $tokenData = $this -> tokenStorage -> exchangeAuthCodeForAccessToken ( $bodyParams [ 'code' ] , function ( array $authCode ) use ( $request , $bodyParams ) {
-
734
-
735 $requiredParameters = ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) ? [ 'client_id' , 'redirect_uri' , 'code_verifier' ] : [ 'client_id' , 'redirect_uri' ] ;
-
736 $missingRequiredParameters = array_filter ( $requiredParameters , function ( $p ) use ( $bodyParams ) {
-
737 return ! array_key_exists ( $p , $bodyParams ) || empty ( $bodyParams [ $p ] ) ;
-
738 } ) ;
-
739 if ( ! empty ( $missingRequiredParameters ) ) {
-
740 $this -> logger -> warning ( 'The exchange request was missing required parameters. Returning an error response.' , [ 'missing' => $missingRequiredParameters ] ) ;
-
741 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST , $request ) ;
-
742 }
-
743
-
744
-
745 if ( $authCode [ 'client_id' ] !== $bodyParams [ 'client_id' ]
-
746 || $authCode [ 'redirect_uri' ] !== $bodyParams [ 'redirect_uri' ] ) {
-
747 $this -> logger -> error ( "The provided client_id and/or redirect_uri did not match those stored in the token." ) ;
-
748 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
749 }
-
750
-
751
-
752
-
753 if ( ! empty ( $bodyParams [ 'code_verifier' ] ) && empty ( $authCode [ 'code_challenge' ] ) ) {
-
754 $this -> logger -> error ( "A code_verifier was provided when trying to exchange an auth code requested without a code_challenge." ) ;
-
755 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
756 }
-
757
-
758 if ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) {
-
759
-
760
-
761 if ( ! hash_equals ( $authCode [ 'code_challenge' ] , generatePKCECodeChallenge ( $bodyParams [ 'code_verifier' ] ) ) ) {
-
762 $this -> logger -> error ( "The provided code_verifier did not hash to the stored code_challenge" ) ;
-
763 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
764 }
-
765 }
-
766
-
767
-
768 if ( empty ( $authCode [ 'scope' ] ) ) {
-
769 $this -> logger -> error ( "An exchange request for a token with an empty scope was sent to the token endpoint." ) ;
-
770 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
-
771 }
-
772 } ) ;
-
773 } catch ( IndieAuthException $e ) {
-
774
-
775 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
776 'error' => $e -> getInfo ( ) [ 'error' ] ,
-
777 'error_description' => $e -> getMessage ( )
-
778 ] ) ) ;
-
779 }
-
780
-
781 if ( is_null ( $tokenData ) ) {
-
782 $this -> logger -> error ( 'Attempting to exchange an auth code for a token resulted in null.' , $bodyParams ) ;
-
783 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
784 'error' => 'invalid_grant' ,
-
785 'error_description' => 'The provided credentials were not valid.'
+
726 if ( ! isset ( $bodyParams [ 'code' ] ) ) {
+
727 $this -> logger -> warning ( 'The exchange request was missing the code parameter. Returning an error response.' ) ;
+
728 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
729 'error' => 'invalid_request' ,
+
730 'error_description' => 'The code parameter was missing.'
+
731 ] ) ) ;
+
732 }
+
733
+
734
+
735
+
736
+
737
+
738 try {
+
739
+
740
+
741 $tokenData = $this -> tokenStorage -> exchangeAuthCodeForAccessToken ( $bodyParams [ 'code' ] , function ( array $authCode ) use ( $request , $bodyParams ) {
+
742
+
743 $requiredParameters = ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) ? [ 'client_id' , 'redirect_uri' , 'code_verifier' ] : [ 'client_id' , 'redirect_uri' ] ;
+
744 $missingRequiredParameters = array_filter ( $requiredParameters , function ( $p ) use ( $bodyParams ) {
+
745 return ! array_key_exists ( $p , $bodyParams ) || empty ( $bodyParams [ $p ] ) ;
+
746 } ) ;
+
747 if ( ! empty ( $missingRequiredParameters ) ) {
+
748 $this -> logger -> warning ( 'The exchange request was missing required parameters. Returning an error response.' , [ 'missing' => $missingRequiredParameters ] ) ;
+
749 throw IndieAuthException :: create ( IndieAuthException :: INVALID_REQUEST , $request ) ;
+
750 }
+
751
+
752
+
753 if ( $authCode [ 'client_id' ] !== $bodyParams [ 'client_id' ]
+
754 || $authCode [ 'redirect_uri' ] !== $bodyParams [ 'redirect_uri' ] ) {
+
755 $this -> logger -> error ( "The provided client_id and/or redirect_uri did not match those stored in the token." ) ;
+
756 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
757 }
+
758
+
759
+
760
+
761 if ( ! empty ( $bodyParams [ 'code_verifier' ] ) && empty ( $authCode [ 'code_challenge' ] ) ) {
+
762 $this -> logger -> error ( "A code_verifier was provided when trying to exchange an auth code requested without a code_challenge." ) ;
+
763 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
764 }
+
765
+
766 if ( $this -> requirePkce or ! empty ( $authCode [ 'code_challenge' ] ) ) {
+
767
+
768
+
769 if ( ! hash_equals ( $authCode [ 'code_challenge' ] , generatePKCECodeChallenge ( $bodyParams [ 'code_verifier' ] ) ) ) {
+
770 $this -> logger -> error ( "The provided code_verifier did not hash to the stored code_challenge" ) ;
+
771 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
772 }
+
773 }
+
774
+
775
+
776 if ( empty ( $authCode [ 'scope' ] ) ) {
+
777 $this -> logger -> error ( "An exchange request for a token with an empty scope was sent to the token endpoint." ) ;
+
778 throw IndieAuthException :: create ( IndieAuthException :: INVALID_GRANT , $request ) ;
+
779 }
+
780 } ) ;
+
781 } catch ( IndieAuthException $e ) {
+
782
+
783 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
784 'error' => $e -> getInfo ( ) [ 'error' ] ,
+
785 'error_description' => $e -> getMessage ( )
786 ] ) ) ;
787 }
788
-
789
-
790
-
791
-
792 return new Response ( 200 , [
-
793 'content-type' => 'application/json' ,
-
794 'cache-control' => 'no-store' ,
-
795 ] , json_encode ( array_merge ( [
-
796
-
797 'token_type' => 'Bearer'
-
798 ] , array_filter ( $tokenData , function ( string $k ) {
-
799
-
800
-
801 return ! in_array ( $k , [ 'code_challenge' , 'code_challenge_method' ] ) ;
-
802 } , ARRAY_FILTER_USE_KEY ) ) ) ) ;
-
803 }
-
804
-
805 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
-
806 'error' => 'invalid_request' ,
-
807 'error_description' => 'Request to token endpoint was not a valid code exchange request.'
-
808 ] ) ) ;
-
809 }
-
810
-
811
-
812
-
813
-
814
-
815
-
816 protected function handleException ( IndieAuthException $exception ) : ResponseInterface {
-
817 $exceptionData = $exception -> getInfo ( ) ;
+
789 if ( is_null ( $tokenData ) ) {
+
790 $this -> logger -> error ( 'Attempting to exchange an auth code for a token resulted in null.' , $bodyParams ) ;
+
791 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
792 'error' => 'invalid_grant' ,
+
793 'error_description' => 'The provided credentials were not valid.'
+
794 ] ) ) ;
+
795 }
+
796
+
797
+
798
+
799
+
800 return new Response ( 200 , [
+
801 'content-type' => 'application/json' ,
+
802 'cache-control' => 'no-store' ,
+
803 ] , json_encode ( array_merge ( [
+
804
+
805 'token_type' => 'Bearer'
+
806 ] , array_filter ( $tokenData , function ( string $k ) {
+
807
+
808
+
809 return ! in_array ( $k , [ 'code_challenge' , 'code_challenge_method' ] ) ;
+
810 } , ARRAY_FILTER_USE_KEY ) ) ) ) ;
+
811 }
+
812
+
813 return new Response ( 400 , [ 'content-type' => 'application/json' ] , json_encode ( [
+
814 'error' => 'invalid_request' ,
+
815 'error_description' => 'Request to token endpoint was not a valid code exchange request.'
+
816 ] ) ) ;
+
817 }
818
-
819 if ( $exceptionData [ 'statusCode' ] == 302 ) {
-
820
-
821 $redirectQueryParams = [
-
822 'error' => $exceptionData [ 'error' ] ?? 'invalid_request' ,
-
823 'error_description' => (string) $exception
-
824 ] ;
-
825
-
826
-
827 if ( $exception -> getCode ( ) !== IndieAuthException :: INVALID_STATE ) {
-
828 $redirectQueryParams [ 'state' ] = $exception -> getRequest ( ) -> getQueryParams ( ) [ 'state' ] ;
-
829 }
-
830
-
831 return new Response ( $exceptionData [ 'statusCode' ] , [
-
832 'Location' => appendQueryParams ( (string) $exception -> getRequest ( ) -> getQueryParams ( ) [ 'redirect_uri' ] , $redirectQueryParams )
-
833 ] ) ;
-
834 } else {
-
835
-
836 return new Response ( $exception -> getStatusCode ( ) , [ 'content-type' => 'text/html' ] , renderTemplate ( $this -> exceptionTemplatePath , [
-
837 'request' => $exception -> getRequest ( ) ,
-
838 'exception' => $exception
-
839 ] ) ) ;
-
840 }
-
841 }
-
842 }
+
819
+
820
+
821
+
822
+
823
+
824 protected function handleException ( IndieAuthException $exception ) : ResponseInterface {
+
825 $exceptionData = $exception -> getInfo ( ) ;
+
826
+
827 if ( $exceptionData [ 'statusCode' ] == 302 ) {
+
828
+
829 $redirectQueryParams = [
+
830 'error' => $exceptionData [ 'error' ] ?? 'invalid_request' ,
+
831 'error_description' => (string) $exception
+
832 ] ;
+
833
+
834
+
835 if ( $exception -> getCode ( ) !== IndieAuthException :: INVALID_STATE ) {
+
836 $redirectQueryParams [ 'state' ] = $exception -> getRequest ( ) -> getQueryParams ( ) [ 'state' ] ;
+
837 }
+
838
+
839 return new Response ( $exceptionData [ 'statusCode' ] , [
+
840 'Location' => appendQueryParams ( (string) $exception -> getRequest ( ) -> getQueryParams ( ) [ 'redirect_uri' ] , $redirectQueryParams )
+
841 ] ) ;
+
842 } else {
+
843
+
844 return new Response ( $exception -> getStatusCode ( ) , [ 'content-type' => 'text/html' ] , renderTemplate ( $this -> exceptionTemplatePath , [
+
845 'request' => $exception -> getRequest ( ) ,
+
846 'exception' => $exception
+
847 ] ) ) ;
+
848 }
+
849 }
+
850 }
@@ -1062,7 +1070,7 @@
Legend
Executed Not Executed Dead Code
- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Thu Jun 17 22:38:11 UTC 2021.
+ Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021.
diff --git a/docs/coverage/Storage/FilesystemJsonStorage.php.html b/docs/coverage/Storage/FilesystemJsonStorage.php.html
index 3fdcd24..eedc1e0 100644
--- a/docs/coverage/Storage/FilesystemJsonStorage.php.html
+++ b/docs/coverage/Storage/FilesystemJsonStorage.php.html
@@ -663,7 +663,7 @@
Legend
Executed Not Executed Dead Code
- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Thu Jun 17 22:38:11 UTC 2021.
+ Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021.
diff --git a/docs/coverage/Storage/Sqlite3Storage.php.html b/docs/coverage/Storage/Sqlite3Storage.php.html
index a4cca04..f05ea4e 100644
--- a/docs/coverage/Storage/Sqlite3Storage.php.html
+++ b/docs/coverage/Storage/Sqlite3Storage.php.html
@@ -79,7 +79,7 @@
Legend
Executed Not Executed Dead Code
- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Thu Jun 17 22:38:11 UTC 2021.
+ Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021.
diff --git a/docs/coverage/Storage/TokenStorageInterface.php.html b/docs/coverage/Storage/TokenStorageInterface.php.html
index b546673..3bcf7e3 100644
--- a/docs/coverage/Storage/TokenStorageInterface.php.html
+++ b/docs/coverage/Storage/TokenStorageInterface.php.html
@@ -235,7 +235,7 @@
Legend
Executed Not Executed Dead Code
- Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Thu Jun 17 22:38:11 UTC 2021.
+ Generated by php-code-coverage 9.2.6 using PHP 7.4.19 with Xdebug 3.0.4 and PHPUnit 9.5.5 at Fri Jun 18 14:09:49 UTC 2021.
diff --git a/docs/coverage/Storage/dashboard.html b/docs/coverage/Storage/dashboard.html
index 267f0c0..24b21b6 100644
--- a/docs/coverage/Storage/dashboard.html
+++ b/docs/coverage/Storage/dashboard.html
@@ -142,7 +142,7 @@
diff --git a/docs/coverage/Storage/index.html b/docs/coverage/Storage/index.html
index 6614179..d21768a 100644
--- a/docs/coverage/Storage/index.html
+++ b/docs/coverage/Storage/index.html
@@ -137,7 +137,7 @@