Fix the anikeen API guard and register its user provider

- Register the guard driver and the anikeen user provider with static closures:
  Laravel binds extend() closures to the AuthManager, so $this->makeGuard() failed.
- Look the token's sub up in the user's Anikeen ID column (anikeen_id by default;
  provider 'column', services.anikeen.user_id_column or useUserIdField()),
  not in the primary key; works with the anikeen, eloquent and database providers.
- JwtParser: key from services.anikeen.public_key(_path) or the bundled key, read
  once per process; misconfiguration is an error, not a 401. Passport tokens have
  no client claim, so it is optional now.
- Optional revocation check against Anikeen ID (verify_revocation), cached per token.
- anikeenTokenCan() without a token, anikeenTokenCanAny(), scope middleware accept
  JSON responses.
- Update the bundled staging public key to the current staging key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbpgPaLqhBzo1AXHNs9Cx5
This commit is contained in:
Maurice PreußandClaude Opus 5.5 committed 2026-11-03 23:07:58 +01:00
1 parent b074ec3c0c
commit dc29eb2844
12 files changed
+554 -163

No files matched your search

+52 -10
View File
@@ -7,32 +7,74 @@ use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use RuntimeException;
use stdClass;
use Throwable;
/**
* Verifies access tokens issued by Anikeen ID (Passport, RS256): signature against
* Anikeen ID's public key plus the time claims (exp, nbf, iat; 60 seconds leeway).
*
* The key is `services.anikeen.public_key` (PEM, "\n" escapes allowed) or the file at
* `services.anikeen.public_key_path`, else the key shipped with this package for the
* configured mode (production / staging). It is read once per process.
*/
class JwtParser
{
/**
* Public keys already read, by source.
*
* @var array<string, string>
*/
private static array $keys = [];
/**
* @throws AuthenticationException
*/
public function decode(Request $request): stdClass
{
return $this->decodeToken((string) $request->bearerToken());
}
/**
* @throws AuthenticationException
*/
public function decodeToken(string $token): stdClass
{
if ($token === '') {
throw new AuthenticationException('No access token given.');
}
JWT::$leeway = 60;
// outside the try: a missing key is a configuration error, not an invalid token
$key = new Key($this->getOauthPublicKey(), 'RS256');
try {
return JWT::decode(
$request->bearerToken(),
new Key($this->getOauthPublicKey(), 'RS256')
);
} catch (Throwable $exception) {
throw (new AuthenticationException());
return JWT::decode($token, $key);
} catch (Throwable) {
throw new AuthenticationException('The access token is invalid or expired.');
}
}
private function getOauthPublicKey(): bool|string
/**
* Forget the keys read so far (e.g. after changing the configuration in tests).
*/
public static function flushKeys(): void
{
return AnikeenId::getMode() === 'staging'
? file_get_contents(dirname(__DIR__, 3) . '/oauth-public.staging.key')
: file_get_contents(dirname(__DIR__, 3) . '/oauth-public.key');
self::$keys = [];
}
protected function getOauthPublicKey(): string
{
if ($key = config('services.anikeen.public_key')) {
return self::$keys['config:' . md5($key)] ??= str_replace('\\n', "\n", $key);
}
$path = config('services.anikeen.public_key_path') ?: (AnikeenId::getMode() === 'staging'
? dirname(__DIR__, 3) . '/oauth-public.staging.key'
: dirname(__DIR__, 3) . '/oauth-public.key');
return self::$keys['file:' . $path] ??= (is_readable($path) ? (string) file_get_contents($path) : throw new RuntimeException("Anikeen ID public key [$path] is not readable."));
}
}