From e456f688a309f7b0ddc67f3924d6aaa56b90eed2 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 24 Sep 2022 18:28:05 +0100 Subject: [PATCH] We need to manually check the indieauth endpoint ourselves now --- .../Controllers/TokenEndpointController.php | 119 +++++++++++++----- routes/web.php | 2 +- tests/Feature/TokenEndpointTest.php | 74 +++++++---- 3 files changed, 135 insertions(+), 60 deletions(-) diff --git a/app/Http/Controllers/TokenEndpointController.php b/app/Http/Controllers/TokenEndpointController.php index af18a8f1..39c32d79 100644 --- a/app/Http/Controllers/TokenEndpointController.php +++ b/app/Http/Controllers/TokenEndpointController.php @@ -5,18 +5,26 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Services\TokenService; +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Exception\BadResponseException; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use IndieAuth\Client; class TokenEndpointController extends Controller { /** - * The IndieAuth Client. + * @var Client The IndieAuth Client. */ protected Client $client; /** - * The Token handling service. + * @var GuzzleClient The GuzzleHttp client. + */ + protected GuzzleClient $guzzle; + + /** + * @var TokenService The Token handling service. */ protected TokenService $tokenService; @@ -24,57 +32,100 @@ class TokenEndpointController extends Controller * Inject the dependencies. * * @param Client $client + * @param GuzzleClient $guzzle * @param TokenService $tokenService */ public function __construct( Client $client, + GuzzleClient $guzzle, TokenService $tokenService ) { $this->client = $client; + $this->guzzle = $guzzle; $this->tokenService = $tokenService; } /** * If the user has auth’d via the IndieAuth protocol, issue a valid token. * + * @param Request $request * @return JsonResponse */ - public function create(): JsonResponse + public function create(Request $request): JsonResponse { - $authorizationEndpoint = $this->client->discoverAuthorizationEndpoint(normalize_url(request()->input('me'))); - if ($authorizationEndpoint) { - $auth = $this->client->verifyIndieAuthCode( - $authorizationEndpoint, - request()->input('code'), - request()->input('me'), - request()->input('redirect_uri'), - request()->input('client_id'), - null // code_verifier - ); - if (array_key_exists('me', $auth)) { - $scope = $auth['scope'] ?? ''; - $tokenData = [ - 'me' => request()->input('me'), - 'client_id' => request()->input('client_id'), - 'scope' => $scope, - ]; - $token = $this->tokenService->getNewToken($tokenData); - $content = [ - 'me' => request()->input('me'), - 'scope' => $scope, - 'access_token' => $token, - ]; - - return response()->json($content); - } - + if (empty($request->input('me'))) { return response()->json([ - 'error' => 'There was an error verifying the authorisation code.', + 'error' => 'Missing {me} param from input', + ], 400); + } + + $authorizationEndpoint = $this->client::discoverAuthorizationEndpoint(normalize_url($request->input('me'))); + + if (empty($authorizationEndpoint)) { + return response()->json([ + 'error' => sprintf('Could not discover the authorization endpoint for %s', $request->input('me')) + ], 400); + } + + $auth = $this->verifyIndieAuthCode( + $authorizationEndpoint, + $request->input('code'), + $request->input('me'), + $request->input('redirect_uri'), + $request->input('client_id'), + ); + + if ($auth === null || !array_key_exists('me', $auth)) { + return response()->json([ + 'error' => 'There was an error verifying the IndieAuth code', ], 401); } - return response()->json([ - 'error' => 'Can’t determine the authorisation endpoint.', - ], 400); + $scope = $auth['scope'] ?? ''; + $tokenData = [ + 'me' => $request->input('me'), + 'client_id' => $request->input('client_id'), + 'scope' => $scope, + ]; + $token = $this->tokenService->getNewToken($tokenData); + $content = [ + 'me' => $request->input('me'), + 'scope' => $scope, + 'access_token' => $token, + ]; + + return response()->json($content); + } + + protected function verifyIndieAuthCode( + string $authorizationEndpoint, + string $code, + string $me, + string $redirectUri, + string $clientId + ): ?array { + try { + $response = $this->guzzle->request('POST', $authorizationEndpoint, [ + 'headers' => [ + 'Accept' => 'application/json', + ], + 'form_params' => [ + 'code' => $code, + 'me' => $me, + 'redirect_uri' => $redirectUri, + 'client_id' => $clientId, + ], + ]); + } catch (BadResponseException) { + return null; + } + + try { + $authData = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return null; + } + + return $authData; } } diff --git a/routes/web.php b/routes/web.php index 7d812aac..4d83f871 100644 --- a/routes/web.php +++ b/routes/web.php @@ -129,7 +129,7 @@ Route::group(['domain' => config('url.longurl')], function () { Route::get('/feed.rss', [FeedsController::class, 'blogRss']); Route::get('/feed.atom', [FeedsController::class, 'blogAtom']); Route::get('/feed.json', [FeedsController::class, 'blogJson']); - Route::get('/feed.jf2', [Feedscontroller::class, 'blogJf2']); + Route::get('/feed.jf2', [FeedsController::class, 'blogJf2']); Route::get('/s/{id}', [ArticlesController::class, 'onlyIdInURL']); Route::get('/{year?}/{month?}', [ArticlesController::class, 'index']); Route::get('/{year}/{month}/{slug}', [ArticlesController::class, 'show']); diff --git a/tests/Feature/TokenEndpointTest.php b/tests/Feature/TokenEndpointTest.php index c9924b68..80264234 100644 --- a/tests/Feature/TokenEndpointTest.php +++ b/tests/Feature/TokenEndpointTest.php @@ -4,32 +4,45 @@ declare(strict_types=1); namespace Tests\Feature; -use IndieAuth\Client; +use Exception; +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use IndieAuth\Client as IndieAuthClient; +use JsonException; use Mockery; use Tests\TestCase; class TokenEndpointTest extends TestCase { - /** @test */ + /** + * @test + * @throws JsonException + * @throws Exception + */ public function tokenEndpointIssuesToken(): void { - $mockClient = Mockery::mock(Client::class); - $mockClient->shouldReceive('discoverAuthorizationEndpoint') + $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class); + $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint') ->with(normalize_url(config('app.url'))) ->once() ->andReturn('https://indieauth.com/auth'); - $mockClient->shouldReceive('verifyIndieAuthCode') - ->andReturn([ + $mockHandler = new MockHandler([ + new \GuzzleHttp\Psr7\Response(200, [], json_encode([ 'me' => config('app.url'), 'scope' => 'create update', - ]); - $this->app->instance(Client::class, $mockClient); + ], JSON_THROW_ON_ERROR)), + ]); + $handlerStack = HandlerStack::create($mockHandler); + $mockGuzzleClient = new GuzzleClient(['handler' => $handlerStack]); + $this->app->instance(IndieAuthClient::class, $mockIndieAuthClient); + $this->app->instance(GuzzleClient::class, $mockGuzzleClient); $response = $this->post('/api/token', [ 'me' => config('app.url'), 'code' => 'abc123', 'redirect_uri' => config('app.url') . '/indieauth-callback', 'client_id' => config('app.url') . '/micropub-client', - 'state' => mt_rand(1000, 10000), + 'state' => random_int(1000, 10000), ]); $response->assertJson([ 'me' => config('app.url'), @@ -37,51 +50,62 @@ class TokenEndpointTest extends TestCase ]); } - /** @test */ + /** + * @test + * @throws JsonException + * @throws Exception + */ public function tokenEndpointReturnsErrorWhenAuthEndpointLacksMeData(): void { - $mockClient = Mockery::mock(Client::class); - $mockClient->shouldReceive('discoverAuthorizationEndpoint') + $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class); + $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint') ->with(normalize_url(config('app.url'))) ->once() ->andReturn('https://indieauth.com/auth'); - $mockClient->shouldReceive('verifyIndieAuthCode') - ->andReturn([ + $mockHandler = new MockHandler([ + new \GuzzleHttp\Psr7\Response(400, [], json_encode([ 'error' => 'error_message', - ]); - $this->app->instance(Client::class, $mockClient); + ], JSON_THROW_ON_ERROR)), + ]); + $handlerStack = HandlerStack::create($mockHandler); + $mockGuzzleClient = new GuzzleClient(['handler' => $handlerStack]); + $this->app->instance(IndieAuthClient::class, $mockIndieAuthClient); + $this->app->instance(GuzzleClient::class, $mockGuzzleClient); $response = $this->post('/api/token', [ 'me' => config('app.url'), 'code' => 'abc123', 'redirect_uri' => config('app.url') . '/indieauth-callback', 'client_id' => config('app.url') . '/micropub-client', - 'state' => mt_rand(1000, 10000), + 'state' => random_int(1000, 10000), ]); $response->assertStatus(401); $response->assertJson([ - 'error' => 'There was an error verifying the authorisation code.', + 'error' => 'There was an error verifying the IndieAuth code', ]); } - /** @test */ + /** + * @test + * @throws Exception + */ public function tokenEndpointReturnsErrorWhenNoAuthEndpointFound(): void { - $mockClient = Mockery::mock(Client::class); - $mockClient->shouldReceive('discoverAuthorizationEndpoint') + $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class); + $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint') ->with(normalize_url(config('app.url'))) ->once() ->andReturn(null); - $this->app->instance(Client::class, $mockClient); + $this->app->instance(IndieAuthClient::class, $mockIndieAuthClient); $response = $this->post('/api/token', [ 'me' => config('app.url'), 'code' => 'abc123', 'redirect_uri' => config('app.url') . '/indieauth-callback', 'client_id' => config('app.url') . '/micropub-client', - 'state' => mt_rand(1000, 10000), + 'state' => random_int(1000, 10000), ]); $response->assertStatus(400); $response->assertJson([ - 'error' => 'Can’t determine the authorisation endpoint.', ] - ); + 'error' => 'Could not discover the authorization endpoint for ' . config('app.url'), + ]); } }