diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php index c6008a9c..2df5d432 100644 --- a/app/Http/Controllers/MicropubController.php +++ b/app/Http/Controllers/MicropubController.php @@ -15,7 +15,6 @@ use App\Services\Micropub\MicropubHandlerRegistry; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Lcobucci\JWT\Token; class MicropubController extends Controller { @@ -135,7 +134,7 @@ class MicropubController extends Controller } // the default response is just to return the token data - /** @var Token $tokenData */ + /** @var array $tokenData */ $tokenData = $request->input('token_data'); return response()->json([ diff --git a/app/Http/Middleware/VerifyMicropubToken.php b/app/Http/Middleware/VerifyMicropubToken.php index 33d2cb12..e61cc67a 100644 --- a/app/Http/Middleware/VerifyMicropubToken.php +++ b/app/Http/Middleware/VerifyMicropubToken.php @@ -5,13 +5,9 @@ declare(strict_types=1); namespace App\Http\Middleware; use App\Http\Responses\MicropubResponses; +use App\Models\MicropubToken; use Closure; use Illuminate\Http\Request; -use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Encoding\CannotDecodeContent; -use Lcobucci\JWT\Token; -use Lcobucci\JWT\Token\InvalidTokenStructure; -use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use Symfony\Component\HttpFoundation\Response; class VerifyMicropubToken @@ -39,15 +35,17 @@ class VerifyMicropubToken ], 401); } - try { - $tokenData = $this->validateToken($rawToken); - } catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) { + $token = MicropubToken::where('token_hash', hash('sha256', $rawToken)) + ->whereNull('revoked_at') + ->first(); + + if (! $token) { $micropubResponses = new MicropubResponses; return $micropubResponses->invalidTokenResponse(); } - if ($tokenData->claims()->has('scope') === false) { + if ($token->scope === '') { $micropubResponses = new MicropubResponses; return $micropubResponses->tokenHasNoScopeResponse(); @@ -56,26 +54,10 @@ class VerifyMicropubToken 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'), + 'me' => $token->me, + 'scope' => $token->scope, + 'client_id' => $token->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/Models/MicropubToken.php b/app/Models/MicropubToken.php new file mode 100644 index 00000000..231237f6 --- /dev/null +++ b/app/Models/MicropubToken.php @@ -0,0 +1,34 @@ + 'datetime', + ]; + } + + public function revoke(): void + { + $this->forceFill(['revoked_at' => now()])->save(); + } + + protected function isRevoked(): Attribute + { + return Attribute::make( + get: fn () => $this->revoked_at !== null, + ); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 224472d1..68367a97 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,10 +7,6 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; -use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Signer\Hmac\Sha256; -use Lcobucci\JWT\Signer\Key\InMemory; -use Lcobucci\JWT\Validation\Constraint\SignedWith; use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; @@ -53,17 +49,6 @@ class AppServiceProvider extends ServiceProvider ); }); - // Configure JWT builder - $this->app->bind('Lcobucci\JWT\Configuration', function () { - $key = InMemory::plainText(config('app.key')); - - $config = Configuration::forSymmetricSigner(new Sha256, $key); - - $config->setValidationConstraints(new SignedWith(new Sha256, $key)); - - return $config; - }); - // Configure HtmlSanitizer $this->app->bind(HtmlSanitizer::class, function () { return new HtmlSanitizer( diff --git a/app/Services/TokenService.php b/app/Services/TokenService.php index 68a9293b..2941c28b 100644 --- a/app/Services/TokenService.php +++ b/app/Services/TokenService.php @@ -5,28 +5,26 @@ declare(strict_types=1); namespace App\Services; use App\Jobs\AddClientToDatabase; -use DateTimeImmutable; -use Lcobucci\JWT\Configuration; +use App\Models\MicropubToken; class TokenService { /** - * Generate a JWT token. + * Generate a new bearer token. */ public function getNewToken(array $data): string { - $config = resolve(Configuration::class); + $token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); - $token = $config->builder() - ->issuedAt(new DateTimeImmutable) - ->withClaim('client_id', $data['client_id']) - ->withClaim('me', $data['me']) - ->withClaim('scope', $data['scope']) - ->withClaim('nonce', bin2hex(random_bytes(8))) - ->getToken($config->signer(), $config->signingKey()); + MicropubToken::create([ + 'token_hash' => hash('sha256', $token), + 'client_id' => $data['client_id'], + 'me' => $data['me'], + 'scope' => $data['scope'], + ]); dispatch(new AddClientToDatabase($data['client_id'])); - return $token->toString(); + return $token; } } diff --git a/composer.json b/composer.json index 520d12e1..5871ee02 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,6 @@ "laravel/horizon": "^5.0", "laravel/scout": "^10.1", "laravel/tinker": "^3.0", - "lcobucci/jwt": "^5.0", "league/commonmark": "^2.0", "league/flysystem-aws-s3-v3": "^3.0", "mf2/mf2": "~0.3", diff --git a/composer.lock b/composer.lock index 6af58a17..4c98c2cf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "23983a4e6a8e79cb9636fe8f0e604eb7", + "content-hash": "a2842cf95580a08ad759a92d74fae3b4", "packages": [ { "name": "aws/aws-crt-php", @@ -2431,79 +2431,6 @@ }, "time": "2026-03-17T14:54:13+00:00" }, - { - "name": "lcobucci/jwt", - "version": "5.6.0", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/jwt.git", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "ext-sodium": "*", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "psr/clock": "^1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/clock": "^3.2", - "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.10.7", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.10", - "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^11.1" - }, - "suggest": { - "lcobucci/clock": ">= 3.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com", - "role": "Developer" - } - ], - "description": "A simple library to work with JSON Web Token and JSON Web Signature", - "keywords": [ - "JWS", - "jwt" - ], - "support": { - "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.6.0" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2025-10-17T11:30:53+00:00" - }, { "name": "league/commonmark", "version": "2.8.3", diff --git a/database/migrations/2026_08_13_120924_create_micropub_tokens_table.php b/database/migrations/2026_08_13_120924_create_micropub_tokens_table.php new file mode 100644 index 00000000..cb37f28b --- /dev/null +++ b/database/migrations/2026_08_13_120924_create_micropub_tokens_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('token_hash')->unique(); + $table->string('client_id'); + $table->string('me'); + $table->string('scope'); + $table->timestamp('revoked_at')->nullable(); + $table->timestamps(); + + $table->index('client_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('micropub_tokens'); + } +}; diff --git a/tests/Feature/TokenServiceTest.php b/tests/Feature/TokenServiceTest.php index 7fe9e854..6643452d 100644 --- a/tests/Feature/TokenServiceTest.php +++ b/tests/Feature/TokenServiceTest.php @@ -4,18 +4,16 @@ declare(strict_types=1); namespace Tests\Feature; +use App\Models\MicropubToken; use App\Services\TokenService; -use DateTimeImmutable; -use Lcobucci\JWT\Configuration; -use Lcobucci\JWT\Signer\Key\InMemory; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; class TokenServiceTest extends TestCase { /** - * Given the token is dependent on a random nonce, the time of creation and - * the APP_KEY, to test, we shall create a token, and then verify it. + * Given the token is dependent on a random value and stored only as a + * hash, to test, we shall create a token, and then verify it. */ #[Test] public function tokenservice_creates_valid_tokens(): void @@ -41,24 +39,29 @@ class TokenServiceTest extends TestCase } #[Test] - public function tokens_with_different_signing_key_are_not_valid(): void + public function unknown_tokens_are_not_valid(): void { + $response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer '.bin2hex(random_bytes(32))]); + + $response->assertJson([ + 'response' => 'error', + 'error' => 'invalid_token', + 'error_description' => 'The provided token did not pass validation', + ]); + } + + #[Test] + public function revoked_tokens_are_not_valid(): void + { + $tokenService = new TokenService; $data = [ 'me' => 'https://example.org', 'client_id' => 'https://quill.p3k.io', 'scope' => 'post', ]; + $token = $tokenService->getNewToken($data); - $config = resolve(Configuration::class); - - $token = $config->builder() - ->issuedAt(new DateTimeImmutable) - ->withClaim('client_id', $data['client_id']) - ->withClaim('me', $data['me']) - ->withClaim('scope', $data['scope']) - ->withClaim('nonce', bin2hex(random_bytes(8))) - ->getToken($config->signer(), InMemory::plainText(random_bytes(32))) - ->toString(); + MicropubToken::where('token_hash', hash('sha256', $token))->firstOrFail()->revoke(); $response = $this->get('/api/post', ['HTTP_Authorization' => 'Bearer '.$token]); diff --git a/tests/TestToken.php b/tests/TestToken.php index 287e2757..21e0b753 100644 --- a/tests/TestToken.php +++ b/tests/TestToken.php @@ -2,53 +2,39 @@ namespace Tests; -use DateTimeImmutable; -use Lcobucci\JWT\Configuration; +use App\Services\TokenService; trait TestToken { public function getToken(): string { - $config = $this->app->make(Configuration::class); - - return $config->builder() - ->issuedAt(new DateTimeImmutable) - ->withClaim('client_id', 'https://quill.p3k.io') - ->withClaim('me', 'http://jonnybarnes.localhost') - ->withClaim('scope', ['create', 'update']) - ->getToken($config->signer(), $config->signingKey()) - ->toString(); + return $this->app->make(TokenService::class)->getNewToken([ + 'client_id' => 'https://quill.p3k.io', + 'me' => 'http://jonnybarnes.localhost', + 'scope' => 'create update', + ]); } public function getTokenWithIncorrectScope(): string { - $config = $this->app->make(Configuration::class); - - return $config->builder() - ->issuedAt(new DateTimeImmutable) - ->withClaim('client_id', 'https://quill.p3k.io') - ->withClaim('me', 'https://jonnybarnes.localhost') - ->withClaim('scope', 'view') - ->getToken($config->signer(), $config->signingKey()) - ->toString(); + return $this->app->make(TokenService::class)->getNewToken([ + 'client_id' => 'https://quill.p3k.io', + 'me' => 'https://jonnybarnes.localhost', + 'scope' => 'view', + ]); } - public function getTokenWithNoScope() + public function getTokenWithNoScope(): string { - $config = $this->app->make(Configuration::class); - - return $config->builder() - ->issuedAt(new DateTimeImmutable) - ->withClaim('client_id', 'https://quill.p3k.io') - ->withClaim('me', 'https://jonnybarnes.localhost') - ->getToken($config->signer(), $config->signingKey()) - ->toString(); + return $this->app->make(TokenService::class)->getNewToken([ + 'client_id' => 'https://quill.p3k.io', + 'me' => 'https://jonnybarnes.localhost', + 'scope' => '', + ]); } - public function getInvalidToken() + public function getInvalidToken(): string { - $token = $this->getToken(); - - return substr($token, 0, -5); + return bin2hex(random_bytes(32)); } }