Refactor micropub token verification
This commit is contained in:
parent
70f90dd456
commit
23c275945a
5 changed files with 101 additions and 136 deletions
|
@ -10,19 +10,14 @@ use App\Models\SyndicationTarget;
|
|||
use App\Services\Micropub\HCardService;
|
||||
use App\Services\Micropub\HEntryService;
|
||||
use App\Services\Micropub\UpdateService;
|
||||
use App\Services\TokenService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Lcobucci\JWT\Encoding\CannotDecodeContent;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
class MicropubController extends Controller
|
||||
{
|
||||
protected TokenService $tokenService;
|
||||
|
||||
protected HEntryService $hentryService;
|
||||
|
||||
protected HCardService $hcardService;
|
||||
|
@ -30,12 +25,10 @@ class MicropubController extends Controller
|
|||
protected UpdateService $updateService;
|
||||
|
||||
public function __construct(
|
||||
TokenService $tokenService,
|
||||
HEntryService $hentryService,
|
||||
HCardService $hcardService,
|
||||
UpdateService $updateService
|
||||
) {
|
||||
$this->tokenService = $tokenService;
|
||||
$this->hentryService = $hentryService;
|
||||
$this->hcardService = $hcardService;
|
||||
$this->updateService = $updateService;
|
||||
|
@ -47,34 +40,24 @@ class MicropubController extends Controller
|
|||
*/
|
||||
public function post(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
$this->logMicropubRequest($request->except('token_data'));
|
||||
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
$this->logMicropubRequest($request->all());
|
||||
/** @var Token $tokenData */
|
||||
$tokenData = $request->input('token_data');
|
||||
|
||||
if (($request->input('h') === 'entry') || ($request->input('type.0') === 'h-entry')) {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
$scopes = $tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
||||
if (! in_array('create', $scopes)) {
|
||||
if (! in_array('create', $scopes, true)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
}
|
||||
$location = $this->hentryService->process($request->all(), $this->getCLientId());
|
||||
|
||||
$location = $this->hentryService->process($request->all(), $tokenData['client_id']);
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
|
@ -83,7 +66,7 @@ class MicropubController extends Controller
|
|||
}
|
||||
|
||||
if ($request->input('h') === 'card' || $request->input('type.0') === 'h-card') {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
$scopes = $tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
@ -101,7 +84,7 @@ class MicropubController extends Controller
|
|||
}
|
||||
|
||||
if ($request->input('action') === 'update') {
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
$scopes = $tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
|
@ -130,12 +113,6 @@ class MicropubController extends Controller
|
|||
*/
|
||||
public function get(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
||||
return (new MicropubResponses)->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($request->input('q') === 'syndicate-to') {
|
||||
return response()->json([
|
||||
'syndicate-to' => SyndicationTarget::all(),
|
||||
|
@ -168,28 +145,18 @@ class MicropubController extends Controller
|
|||
}
|
||||
|
||||
// default response is just to return the token data
|
||||
/** @var Token $tokenData */
|
||||
$tokenData = $request->input('token_data');
|
||||
return response()->json([
|
||||
'response' => 'token',
|
||||
'token' => [
|
||||
'me' => $tokenData->claims()->get('me'),
|
||||
'scope' => $tokenData->claims()->get('scope'),
|
||||
'client_id' => $tokenData->claims()->get('client_id'),
|
||||
'me' => $tokenData['me'],
|
||||
'scope' => $tokenData['scope'],
|
||||
'client_id' => $tokenData['client_id'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the client id from the access token sent with the request.
|
||||
*
|
||||
* @throws RequiredConstraintsViolated
|
||||
*/
|
||||
private function getClientId(): string
|
||||
{
|
||||
return resolve(TokenService::class)
|
||||
->validateToken(app('request')->input('access_token'))
|
||||
->claims()->get('client_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the details of the micropub request to a log file.
|
||||
*/
|
||||
|
|
|
@ -7,10 +7,8 @@ namespace App\Http\Controllers;
|
|||
use App\Http\Responses\MicropubResponses;
|
||||
use App\Jobs\ProcessMedia;
|
||||
use App\Models\Media;
|
||||
use App\Services\TokenService;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
@ -18,43 +16,20 @@ use Illuminate\Http\UploadedFile;
|
|||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class MicropubMediaController extends Controller
|
||||
{
|
||||
protected TokenService $tokenService;
|
||||
|
||||
public function __construct(TokenService $tokenService)
|
||||
{
|
||||
$this->tokenService = $tokenService;
|
||||
}
|
||||
|
||||
public function getHandler(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
$tokenData = $request->input('token_data');
|
||||
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
$scopes = $tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
if (! in_array('create', $scopes, true)) {
|
||||
return (new MicropubResponses)->insufficientScopeResponse();
|
||||
}
|
||||
|
||||
if ($request->input('q') === 'last') {
|
||||
|
@ -105,28 +80,14 @@ class MicropubMediaController extends Controller
|
|||
*/
|
||||
public function media(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
$tokenData = $request->input('token_data');
|
||||
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
$scopes = $tokenData->claims()->get('scope');
|
||||
$scopes = $tokenData['scope'];
|
||||
if (is_string($scopes)) {
|
||||
$scopes = explode(' ', $scopes);
|
||||
}
|
||||
if (! in_array('create', $scopes)) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->insufficientScopeResponse();
|
||||
if (! in_array('create', $scopes, true)) {
|
||||
return (new MicropubResponses)->insufficientScopeResponse();
|
||||
}
|
||||
|
||||
if ($request->hasFile('file') === false) {
|
||||
|
@ -161,7 +122,7 @@ class MicropubMediaController extends Controller
|
|||
}
|
||||
|
||||
$media = Media::create([
|
||||
'token' => $request->bearerToken(),
|
||||
'token' => $request->input('access_token'),
|
||||
'path' => $filename,
|
||||
'type' => $this->getFileTypeFromMimeType($request->file('file')->getMimeType()),
|
||||
'image_widths' => $width,
|
||||
|
|
|
@ -4,31 +4,78 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Responses\MicropubResponses;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Lcobucci\JWT\Encoding\CannotDecodeContent;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Lcobucci\JWT\Configuration;
|
||||
|
||||
class VerifyMicropubToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$rawToken = null;
|
||||
|
||||
if ($request->input('access_token')) {
|
||||
return $next($request);
|
||||
$rawToken = $request->input('access_token');
|
||||
} elseif ($request->bearerToken()) {
|
||||
$rawToken = $request->bearerToken();
|
||||
}
|
||||
|
||||
if ($request->bearerToken()) {
|
||||
return $next($request->merge([
|
||||
'access_token' => $request->bearerToken(),
|
||||
]));
|
||||
if (! $rawToken) {
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No access token was provided in the request',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'error',
|
||||
'error' => 'unauthorized',
|
||||
'error_description' => 'No access token was provided in the request',
|
||||
], 401);
|
||||
try {
|
||||
$tokenData = $this->validateToken($rawToken);
|
||||
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->invalidTokenResponse();
|
||||
}
|
||||
|
||||
if ($tokenData->claims()->has('scope') === false) {
|
||||
$micropubResponses = new MicropubResponses;
|
||||
|
||||
return $micropubResponses->tokenHasNoScopeResponse();
|
||||
}
|
||||
|
||||
return $next($request->merge([
|
||||
'access_token' => $rawToken,
|
||||
'token_data' => [
|
||||
'me' => $tokenData->claims()->get('me'),
|
||||
'scope' => $tokenData->claims()->get('scope'),
|
||||
'client_id' => $tokenData->claims()->get('client_id'),
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the token signature is valid.
|
||||
*/
|
||||
private function validateToken(string $bearerToken): Token
|
||||
{
|
||||
$config = resolve(Configuration::class);
|
||||
|
||||
$token = $config->parser()->parse($bearerToken);
|
||||
|
||||
$constraints = $config->validationConstraints();
|
||||
|
||||
$config->validator()->assert($token, ...$constraints);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace App\Services;
|
|||
use App\Jobs\AddClientToDatabase;
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\Configuration;
|
||||
use Lcobucci\JWT\Token;
|
||||
|
||||
class TokenService
|
||||
{
|
||||
|
@ -30,20 +29,4 @@ class TokenService
|
|||
|
||||
return $token->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the token signature is valid.
|
||||
*/
|
||||
public function validateToken(string $bearerToken): Token
|
||||
{
|
||||
$config = resolve('Lcobucci\JWT\Configuration');
|
||||
|
||||
$token = $config->parser()->parse($bearerToken);
|
||||
|
||||
$constraints = $config->validationConstraints();
|
||||
|
||||
$config->validator()->assert($token, ...$constraints);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class TokenServiceTest extends TestCase
|
|||
* the APP_KEY, to test, we shall create a token, and then verify it.
|
||||
*/
|
||||
#[Test]
|
||||
public function tokenservice_creates_and_validates_tokens(): void
|
||||
public function tokenservice_creates_valid_tokens(): void
|
||||
{
|
||||
$tokenService = new TokenService;
|
||||
$data = [
|
||||
|
@ -28,20 +28,22 @@ class TokenServiceTest extends TestCase
|
|||
'scope' => 'post',
|
||||
];
|
||||
$token = $tokenService->getNewToken($data);
|
||||
$valid = $tokenService->validateToken($token);
|
||||
$validData = [
|
||||
'me' => $valid->claims()->get('me'),
|
||||
'client_id' => $valid->claims()->get('client_id'),
|
||||
'scope' => $valid->claims()->get('scope'),
|
||||
];
|
||||
$this->assertSame($data, $validData);
|
||||
|
||||
$response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer ' . $token]);
|
||||
|
||||
$response->assertJson([
|
||||
'response' => 'token',
|
||||
'token' => [
|
||||
'me' => $data['me'],
|
||||
'client_id' => $data['client_id'],
|
||||
'scope' => $data['scope'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function tokens_with_different_signing_key_throws_exception(): void
|
||||
public function tokens_with_different_signing_key_are_not_valid(): void
|
||||
{
|
||||
$this->expectException(RequiredConstraintsViolated::class);
|
||||
|
||||
$data = [
|
||||
'me' => 'https://example.org',
|
||||
'client_id' => 'https://quill.p3k.io',
|
||||
|
@ -59,7 +61,12 @@ class TokenServiceTest extends TestCase
|
|||
->getToken($config->signer(), InMemory::plainText(random_bytes(32)))
|
||||
->toString();
|
||||
|
||||
$service = new TokenService;
|
||||
$service->validateToken($token);
|
||||
$response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer ' . $token]);
|
||||
|
||||
$response->assertJson([
|
||||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue