paragonie/paseto
Reference PHP implementation of PASETO security tokens (v3/v4): safer alternative to JWT/JWE/JWS with modern crypto. Supports local and public tokens, includes PASERK integration for key serialization/wrapping, and works with Sodium (or sodium_compat).
Installation:
composer require paragonie/paseto
Ensure PHP 8.1+ with GMP, OpenSSL, and Sodium (or sodium_compat) extensions.
First Use Case: Generate a symmetric key and create a local token:
use ParagonIE\Paseto\Builder;
use ParagonIE\Paseto\Keys\Base\SymmetricKey;
$key = SymmetricKey::v4();
$token = Builder::getLocal($key)
->setClaims(['user_id' => 123])
->toString();
Verify a Token:
use ParagonIE\Paseto\Parser;
$parser = Parser::getLocal($key);
$decoded = $parser->parse($token);
// Symmetric (local) token
$token = Builder::getLocal($key)
->setClaims(['role' => 'admin'])
->setExpiration((new DateTime())->add(new DateInterval('P1D')))
->toString();
// Asymmetric (public) token with PASERK
$publicKey = $privateKey->getPublicKey();
$token = Builder::getPublic($publicKey)
->setClaims(['email' => 'user@example.com'])
->toString();
$parser = Parser::getLocal($key)
->addRule(new \ParagonIE\Paseto\Rules\ValidAt) // Check `iat`, `nbf`, `exp`
->addRule(new \ParagonIE\Paseto\Rules\IssuedBy('my-app'))
->addRule(new \ParagonIE\Paseto\Rules\FooterJSON->setMaxKeys(5));
try {
$decoded = $parser->parse($token);
} catch (\ParagonIE\Paseto\Exception\PasetoException $e) {
// Handle invalid tokens (e.g., expired, tampered)
}
// Receiving key ring for public tokens
$keyRing = new \ParagonIE\Paseto\ReceivingKeyRing()
->setVersion(new \ParagonIE\Paseto\Protocol\Version4())
->setPurpose(\ParagonIE\Paseto\Purpose::public())
->addKey('key-v1', $publicKeyV1)
->addKey('key-v2', $publicKeyV2);
$parser = Parser::getPublic($keyRing);
$decoded = $parser->parse($token); // Automatically tries all keys
// Builder
$token = Builder::getLocal($key)
->setImplicitAssertions(['tenant_id' => 42])
->setClaims(['user_id' => 123])
->toString();
// Parser (fails if tenant_id mismatch)
$parser = Parser::getLocal($key)
->setImplicitAssertions(['tenant_id' => 42]);
$parser->parse($token);
Laravel Integration:
.env or Laravel’s config:
// config/paseto.php
return [
'keys' => [
'local' => env('PASETO_LOCAL_KEY'),
'public' => env('PASETO_PUBLIC_KEY'),
],
];
// app/Helpers/Paseto.php
use ParagonIE\Paseto\Builder;
use ParagonIE\Paseto\Keys\Base\SymmetricKey;
class PasetoHelper {
public static function generateLocalToken(array $claims): string {
$key = SymmetricKey::fromString(config('paseto.keys.local'));
return Builder::getLocal($key)
->setClaims($claims)
->toString();
}
}
Middleware for API Auth:
// app/Http/Middleware/VerifyPasetoToken.php
use ParagonIE\Paseto\Parser;
use ParagonIE\Paseto\Keys\Base\SymmetricKey;
class VerifyPasetoToken {
public function handle($request, Closure $next) {
$token = $request->bearerToken();
$key = SymmetricKey::fromString(config('paseto.keys.local'));
$parser = Parser::getLocal($key)
->addRule(new \ParagonIE\Paseto\Rules\ValidAt);
$decoded = $parser->parse($token);
$request->merge(['user' => $decoded->getClaims()]);
return $next($request);
}
}
Queue Jobs with Tokens:
// Dispatch a job with a token payload
$token = PasetoHelper::generateLocalToken([
'job' => 'ProcessOrder',
'order_id' => 123,
]);
ProcessOrderJob::dispatch($token);
// In the job
$parser = Parser::getLocal($key);
$claims = $parser->parse($token)->getClaims();
Footers for Metadata:
// Builder
$token = Builder::getLocal($key)
->setClaims(['user_id' => 1])
->setFooterArray(['ip' => '192.168.1.1', 'user_agent' => 'Chrome'])
->toString();
// Parser (with rules)
$parser = Parser::getLocal($key)
->addRule(new \ParagonIE\Paseto\Rules\FooterJSON);
$footer = $parser->parse($token)->getFooterArray();
Key Version Mismatch:
TypeError if using a SymmetricKey with Version4 directly (must use Builder).Builder::getLocal($key) or Builder::getPublic($key).Sodium Extension Missing:
sodium_compat (slower, less secure).pecl install sodium or use a Docker image with Sodium preinstalled.Implicit Assertions Not Set:
$builder->setImplicitAssertions(['tenant_id' => 42]);
$parser->setImplicitAssertions(['tenant_id' => 42]);
Footer JSON Rules Too Lenient:
$parser->addRule(
new \ParagonIE\Paseto\Rules\FooterJSON()
->setMaxLength(1024)
->setMaxKeys(10)
);
Key Rotation Gaps:
$keyRing->addKey('old-key', $oldPublicKey);
$keyRing->addKey('new-key', $newPublicKey);
Token Inspection:
$tokenParts = explode('.', $token);
// $tokenParts[0] = version.purpose
// $tokenParts[1] = ciphertext (local) or signature (public)
Parser Errors:
PasetoException and log the getMessage() for debugging:
try {
$parser->parse($token);
} catch (PasetoException $e) {
\Log::error('Paseto error: ' . $e->getMessage());
}
Key Validation:
if (!$key->isValidForVersion(new \ParagonIE\Paseto\Protocol\Version4())) {
throw new \RuntimeException('Invalid key for version');
}
\ParagonIE\Paseto\Rules\AbstractRule to validate claims:
class CustomRule extends AbstractRule {
public function validate(JsonToken $token): void {
if ($token->getClaims()['role'] !== 'admin') {
throw new PasetoException
How can I help you explore Laravel packages today?