Replace JWT Micropub tokens with revocable opaque tokens
Tokens now store a hashed row in micropub_tokens instead of being self-contained signed JWTs, so a leaked or unwanted token can actually be revoked. Since revocation already requires a DB lookup on every request, JWT's stateless-verification benefit was gone anyway, so this also drops the lcobucci/jwt dependency entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014625MfkGZ7GVdbqKme4a8L
This commit is contained in:
parent
f9f2744fad
commit
d5706b5f8f
10 changed files with 124 additions and 181 deletions
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
app/Models/MicropubToken.php
Normal file
34
app/Models/MicropubToken.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Table('micropub_tokens')]
|
||||
#[Fillable(['token_hash', 'client_id', 'me', 'scope'])]
|
||||
class MicropubToken extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'revoked_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function revoke(): void
|
||||
{
|
||||
$this->forceFill(['revoked_at' => now()])->save();
|
||||
}
|
||||
|
||||
protected function isRevoked(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->revoked_at !== null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue