We need to manually check the indieauth endpoint ourselves now
This commit is contained in:
parent
b93a8587a3
commit
e456f688a3
3 changed files with 135 additions and 60 deletions
|
@ -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 auth’d via the IndieAuth protocol, issue a valid token.
|
* If the user has auth’d 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' => 'Can’t determine the authorisation endpoint.',
|
$authData = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
], 400);
|
} catch (\JsonException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $authData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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']);
|
||||||
|
|
|
@ -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' => 'Can’t determine the authorisation endpoint.', ]
|
'error' => 'Could not discover the authorization endpoint for ' . config('app.url'),
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue