We need to manually check the indieauth endpoint ourselves now

This commit is contained in:
Jonny Barnes 2022-09-24 18:28:05 +01:00
parent b93a8587a3
commit e456f688a3
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8
3 changed files with 135 additions and 60 deletions

View file

@ -5,18 +5,26 @@ declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\TokenService; use App\Services\TokenService;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use IndieAuth\Client; use IndieAuth\Client;
class TokenEndpointController extends Controller class TokenEndpointController extends Controller
{ {
/** /**
* The IndieAuth Client. * @var Client The IndieAuth Client.
*/ */
protected Client $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; protected TokenService $tokenService;
@ -24,43 +32,64 @@ class TokenEndpointController extends Controller
* Inject the dependencies. * Inject the dependencies.
* *
* @param Client $client * @param Client $client
* @param GuzzleClient $guzzle
* @param TokenService $tokenService * @param TokenService $tokenService
*/ */
public function __construct( public function __construct(
Client $client, Client $client,
GuzzleClient $guzzle,
TokenService $tokenService TokenService $tokenService
) { ) {
$this->client = $client; $this->client = $client;
$this->guzzle = $guzzle;
$this->tokenService = $tokenService; $this->tokenService = $tokenService;
} }
/** /**
* If the user has authd via the IndieAuth protocol, issue a valid token. * If the user has authd via the IndieAuth protocol, issue a valid token.
* *
* @param Request $request
* @return JsonResponse * @return JsonResponse
*/ */
public function create(): JsonResponse public function create(Request $request): JsonResponse
{ {
$authorizationEndpoint = $this->client->discoverAuthorizationEndpoint(normalize_url(request()->input('me'))); if (empty($request->input('me'))) {
if ($authorizationEndpoint) { return response()->json([
$auth = $this->client->verifyIndieAuthCode( '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, $authorizationEndpoint,
request()->input('code'), $request->input('code'),
request()->input('me'), $request->input('me'),
request()->input('redirect_uri'), $request->input('redirect_uri'),
request()->input('client_id'), $request->input('client_id'),
null // code_verifier
); );
if (array_key_exists('me', $auth)) {
if ($auth === null || !array_key_exists('me', $auth)) {
return response()->json([
'error' => 'There was an error verifying the IndieAuth code',
], 401);
}
$scope = $auth['scope'] ?? ''; $scope = $auth['scope'] ?? '';
$tokenData = [ $tokenData = [
'me' => request()->input('me'), 'me' => $request->input('me'),
'client_id' => request()->input('client_id'), 'client_id' => $request->input('client_id'),
'scope' => $scope, 'scope' => $scope,
]; ];
$token = $this->tokenService->getNewToken($tokenData); $token = $this->tokenService->getNewToken($tokenData);
$content = [ $content = [
'me' => request()->input('me'), 'me' => $request->input('me'),
'scope' => $scope, 'scope' => $scope,
'access_token' => $token, 'access_token' => $token,
]; ];
@ -68,13 +97,35 @@ class TokenEndpointController extends Controller
return response()->json($content); return response()->json($content);
} }
return response()->json([ protected function verifyIndieAuthCode(
'error' => 'There was an error verifying the authorisation code.', string $authorizationEndpoint,
], 401); 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;
} }
return response()->json([ try {
'error' => 'Cant determine the authorisation endpoint.', $authData = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
], 400); } catch (\JsonException) {
return null;
}
return $authData;
} }
} }

View file

@ -129,7 +129,7 @@ Route::group(['domain' => config('url.longurl')], function () {
Route::get('/feed.rss', [FeedsController::class, 'blogRss']); Route::get('/feed.rss', [FeedsController::class, 'blogRss']);
Route::get('/feed.atom', [FeedsController::class, 'blogAtom']); Route::get('/feed.atom', [FeedsController::class, 'blogAtom']);
Route::get('/feed.json', [FeedsController::class, 'blogJson']); 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('/s/{id}', [ArticlesController::class, 'onlyIdInURL']);
Route::get('/{year?}/{month?}', [ArticlesController::class, 'index']); Route::get('/{year?}/{month?}', [ArticlesController::class, 'index']);
Route::get('/{year}/{month}/{slug}', [ArticlesController::class, 'show']); Route::get('/{year}/{month}/{slug}', [ArticlesController::class, 'show']);

View file

@ -4,32 +4,45 @@ declare(strict_types=1);
namespace Tests\Feature; 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 Mockery;
use Tests\TestCase; use Tests\TestCase;
class TokenEndpointTest extends TestCase class TokenEndpointTest extends TestCase
{ {
/** @test */ /**
* @test
* @throws JsonException
* @throws Exception
*/
public function tokenEndpointIssuesToken(): void public function tokenEndpointIssuesToken(): void
{ {
$mockClient = Mockery::mock(Client::class); $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint') $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url'))) ->with(normalize_url(config('app.url')))
->once() ->once()
->andReturn('https://indieauth.com/auth'); ->andReturn('https://indieauth.com/auth');
$mockClient->shouldReceive('verifyIndieAuthCode') $mockHandler = new MockHandler([
->andReturn([ new \GuzzleHttp\Psr7\Response(200, [], json_encode([
'me' => config('app.url'), 'me' => config('app.url'),
'scope' => 'create update', 'scope' => 'create update',
], JSON_THROW_ON_ERROR)),
]); ]);
$this->app->instance(Client::class, $mockClient); $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', [ $response = $this->post('/api/token', [
'me' => config('app.url'), 'me' => config('app.url'),
'code' => 'abc123', 'code' => 'abc123',
'redirect_uri' => config('app.url') . '/indieauth-callback', 'redirect_uri' => config('app.url') . '/indieauth-callback',
'client_id' => config('app.url') . '/micropub-client', 'client_id' => config('app.url') . '/micropub-client',
'state' => mt_rand(1000, 10000), 'state' => random_int(1000, 10000),
]); ]);
$response->assertJson([ $response->assertJson([
'me' => config('app.url'), 'me' => config('app.url'),
@ -37,51 +50,62 @@ class TokenEndpointTest extends TestCase
]); ]);
} }
/** @test */ /**
* @test
* @throws JsonException
* @throws Exception
*/
public function tokenEndpointReturnsErrorWhenAuthEndpointLacksMeData(): void public function tokenEndpointReturnsErrorWhenAuthEndpointLacksMeData(): void
{ {
$mockClient = Mockery::mock(Client::class); $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint') $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url'))) ->with(normalize_url(config('app.url')))
->once() ->once()
->andReturn('https://indieauth.com/auth'); ->andReturn('https://indieauth.com/auth');
$mockClient->shouldReceive('verifyIndieAuthCode') $mockHandler = new MockHandler([
->andReturn([ new \GuzzleHttp\Psr7\Response(400, [], json_encode([
'error' => 'error_message', 'error' => 'error_message',
], JSON_THROW_ON_ERROR)),
]); ]);
$this->app->instance(Client::class, $mockClient); $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', [ $response = $this->post('/api/token', [
'me' => config('app.url'), 'me' => config('app.url'),
'code' => 'abc123', 'code' => 'abc123',
'redirect_uri' => config('app.url') . '/indieauth-callback', 'redirect_uri' => config('app.url') . '/indieauth-callback',
'client_id' => config('app.url') . '/micropub-client', 'client_id' => config('app.url') . '/micropub-client',
'state' => mt_rand(1000, 10000), 'state' => random_int(1000, 10000),
]); ]);
$response->assertStatus(401); $response->assertStatus(401);
$response->assertJson([ $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 public function tokenEndpointReturnsErrorWhenNoAuthEndpointFound(): void
{ {
$mockClient = Mockery::mock(Client::class); $mockIndieAuthClient = Mockery::mock(IndieAuthClient::class);
$mockClient->shouldReceive('discoverAuthorizationEndpoint') $mockIndieAuthClient->shouldReceive('discoverAuthorizationEndpoint')
->with(normalize_url(config('app.url'))) ->with(normalize_url(config('app.url')))
->once() ->once()
->andReturn(null); ->andReturn(null);
$this->app->instance(Client::class, $mockClient); $this->app->instance(IndieAuthClient::class, $mockIndieAuthClient);
$response = $this->post('/api/token', [ $response = $this->post('/api/token', [
'me' => config('app.url'), 'me' => config('app.url'),
'code' => 'abc123', 'code' => 'abc123',
'redirect_uri' => config('app.url') . '/indieauth-callback', 'redirect_uri' => config('app.url') . '/indieauth-callback',
'client_id' => config('app.url') . '/micropub-client', 'client_id' => config('app.url') . '/micropub-client',
'state' => mt_rand(1000, 10000), 'state' => random_int(1000, 10000),
]); ]);
$response->assertStatus(400); $response->assertStatus(400);
$response->assertJson([ $response->assertJson([
'error' => 'Cant determine the authorisation endpoint.', ] 'error' => 'Could not discover the authorization endpoint for ' . config('app.url'),
); ]);
} }
} }