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;
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 authd 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' => 'Cant 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;
}
}

View file

@ -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']);

View file

@ -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' => 'Cant determine the authorisation endpoint.', ]
);
'error' => 'Could not discover the authorization endpoint for ' . config('app.url'),
]);
}
}