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