diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php index 244631a6..6031c054 100644 --- a/app/Http/Controllers/MicropubController.php +++ b/app/Http/Controllers/MicropubController.php @@ -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. */ diff --git a/app/Http/Controllers/MicropubMediaController.php b/app/Http/Controllers/MicropubMediaController.php index 430ba3ae..fc804ea2 100644 --- a/app/Http/Controllers/MicropubMediaController.php +++ b/app/Http/Controllers/MicropubMediaController.php @@ -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, diff --git a/app/Http/Middleware/VerifyMicropubToken.php b/app/Http/Middleware/VerifyMicropubToken.php index 813350cf..d96edc8e 100644 --- a/app/Http/Middleware/VerifyMicropubToken.php +++ b/app/Http/Middleware/VerifyMicropubToken.php @@ -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; } } diff --git a/app/Services/TokenService.php b/app/Services/TokenService.php index d0240f6a..68a9293b 100644 --- a/app/Services/TokenService.php +++ b/app/Services/TokenService.php @@ -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; - } } diff --git a/tests/Feature/TokenServiceTest.php b/tests/Feature/TokenServiceTest.php index 55024adf..1c523939 100644 --- a/tests/Feature/TokenServiceTest.php +++ b/tests/Feature/TokenServiceTest.php @@ -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', + ]); } }