Switched to the railgun/jwt library.

This commit is contained in:
flash 2025-05-13 17:27:04 +00:00
parent dd8ec7c8dd
commit 40a96029ff
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
13 changed files with 140 additions and 552 deletions

View file

@ -1 +1 @@
20250423
20250513

View file

@ -3,6 +3,7 @@
"php": ">=8.4",
"ext-mbstring": "*",
"flashwave/index": "^0.2504",
"railgun/jwt": "^0.3",
"erusev/parsedown": "~1.7",
"chillerlan/php-qrcode": "~5.0",
"symfony/mailer": "~7.2",

197
composer.lock generated
View file

@ -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": "600e14fc77ff00755761d6e9b6add998",
"content-hash": "f8bb09ccbe8e19d3a29c44feac42d9c9",
"packages": [
{
"name": "carbonphp/carbon-doctrine-types",
@ -1129,16 +1129,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.9.0",
"version": "3.9.1",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "6d16a8a015166fe54e22c042e0805c5363aef50d"
"reference": "ced71f79398ece168e24f7f7710462f462310d4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6d16a8a015166fe54e22c042e0805c5363aef50d",
"reference": "6d16a8a015166fe54e22c042e0805c5363aef50d",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d",
"reference": "ced71f79398ece168e24f7f7710462f462310d4d",
"shasum": ""
},
"require": {
@ -1231,7 +1231,7 @@
"type": "tidelift"
}
],
"time": "2025-03-27T12:57:33+00:00"
"time": "2025-05-01T19:51:51+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@ -1952,6 +1952,48 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "railgun/jwt",
"version": "v0.3.0",
"source": {
"type": "git",
"url": "https://patchii.net/railgun/jwt.git",
"reference": "9ece83c6eb26b6d052bd9e9c13d27e500befeb98"
},
"require": {
"guzzlehttp/guzzle": "~7.9",
"guzzlehttp/psr7": "~2.7",
"php": "^7.2.5 || ^8.0",
"phpseclib/phpseclib": "~3.0",
"psr/http-client": "~1.0",
"psr/http-factory": "~1.1",
"psr/http-message": "~2.0"
},
"type": "library",
"autoload": {
"files": [
"polyfill.php"
],
"psr-4": {
"Railgun\\Jwt\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"bsd-3-clause-clear"
],
"authors": [
{
"name": "flashwave",
"email": "packagist@flash.moe",
"homepage": "https://flash.moe",
"role": "mom"
}
],
"description": "A modular JWT library.",
"homepage": "https://railgun.sh/libs/jwt",
"time": "2025-05-13T16:16:20+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@ -2053,16 +2095,16 @@
},
{
"name": "sentry/sentry",
"version": "4.11.0",
"version": "4.11.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/sentry-php.git",
"reference": "ebf67deb9902b6da58a4b3383cbd12fed3f4f555"
"reference": "53dc0bcb6a667cac5b760b46f98d5380e63e02ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/ebf67deb9902b6da58a4b3383cbd12fed3f4f555",
"reference": "ebf67deb9902b6da58a4b3383cbd12fed3f4f555",
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/53dc0bcb6a667cac5b760b46f98d5380e63e02ca",
"reference": "53dc0bcb6a667cac5b760b46f98d5380e63e02ca",
"shasum": ""
},
"require": {
@ -2126,7 +2168,7 @@
],
"support": {
"issues": "https://github.com/getsentry/sentry-php/issues",
"source": "https://github.com/getsentry/sentry-php/tree/4.11.0"
"source": "https://github.com/getsentry/sentry-php/tree/4.11.1"
},
"funding": [
{
@ -2138,7 +2180,7 @@
"type": "custom"
}
],
"time": "2025-04-14T09:04:23+00:00"
"time": "2025-05-12T11:30:33+00:00"
},
{
"name": "symfony/clock",
@ -2439,16 +2481,16 @@
},
{
"name": "symfony/mailer",
"version": "v7.2.3",
"version": "v7.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3"
"reference": "998692469d6e698c6eadc7ef37a6530a9eabb356"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3",
"reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3",
"url": "https://api.github.com/repos/symfony/mailer/zipball/998692469d6e698c6eadc7ef37a6530a9eabb356",
"reference": "998692469d6e698c6eadc7ef37a6530a9eabb356",
"shasum": ""
},
"require": {
@ -2499,7 +2541,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v7.2.3"
"source": "https://github.com/symfony/mailer/tree/v7.2.6"
},
"funding": [
{
@ -2515,20 +2557,20 @@
"type": "tidelift"
}
],
"time": "2025-01-27T11:08:17+00:00"
"time": "2025-04-04T09:50:51+00:00"
},
{
"name": "symfony/mime",
"version": "v7.2.4",
"version": "v7.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
"reference": "706e65c72d402539a072d0d6ad105fff6c161ef1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
"url": "https://api.github.com/repos/symfony/mime/zipball/706e65c72d402539a072d0d6ad105fff6c161ef1",
"reference": "706e65c72d402539a072d0d6ad105fff6c161ef1",
"shasum": ""
},
"require": {
@ -2583,7 +2625,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.2.4"
"source": "https://github.com/symfony/mime/tree/v7.2.6"
},
"funding": [
{
@ -2599,7 +2641,7 @@
"type": "tidelift"
}
],
"time": "2025-02-19T08:51:20+00:00"
"time": "2025-04-27T13:34:41+00:00"
},
{
"name": "symfony/options-resolver",
@ -2670,7 +2712,7 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@ -2729,7 +2771,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
},
"funding": [
{
@ -2749,16 +2791,16 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
"reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"shasum": ""
},
"require": {
@ -2812,7 +2854,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
},
"funding": [
{
@ -2828,11 +2870,11 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-09-10T14:38:51+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -2893,7 +2935,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
},
"funding": [
{
@ -2913,19 +2955,20 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
@ -2973,7 +3016,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
},
"funding": [
{
@ -2989,20 +3032,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
@ -3053,7 +3096,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
},
"funding": [
{
@ -3069,11 +3112,11 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.31.0",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
@ -3129,7 +3172,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
},
"funding": [
{
@ -3232,16 +3275,16 @@
},
{
"name": "symfony/translation",
"version": "v7.2.4",
"version": "v7.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
"reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
"url": "https://api.github.com/repos/symfony/translation/zipball/e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6",
"reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6",
"shasum": ""
},
"require": {
@ -3307,7 +3350,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.2.4"
"source": "https://github.com/symfony/translation/tree/v7.2.6"
},
"funding": [
{
@ -3323,7 +3366,7 @@
"type": "tidelift"
}
],
"time": "2025-02-13T10:27:23+00:00"
"time": "2025-04-07T19:09:28+00:00"
},
{
"name": "symfony/translation-contracts",
@ -3405,16 +3448,16 @@
},
{
"name": "twig/html-extra",
"version": "v3.20.0",
"version": "v3.21.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/html-extra.git",
"reference": "f7d54d4de1b64182af745cfb66777f699b599734"
"reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/f7d54d4de1b64182af745cfb66777f699b599734",
"reference": "f7d54d4de1b64182af745cfb66777f699b599734",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/5442dd707601c83b8cd4233e37bb10ab8489a90f",
"reference": "5442dd707601c83b8cd4233e37bb10ab8489a90f",
"shasum": ""
},
"require": {
@ -3457,7 +3500,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/html-extra/tree/v3.20.0"
"source": "https://github.com/twigphp/html-extra/tree/v3.21.0"
},
"funding": [
{
@ -3469,20 +3512,20 @@
"type": "tidelift"
}
],
"time": "2025-01-31T20:45:36+00:00"
"time": "2025-02-19T14:29:33+00:00"
},
{
"name": "twig/twig",
"version": "v3.20.0",
"version": "v3.21.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "3468920399451a384bef53cf7996965f7cd40183"
"reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183",
"reference": "3468920399451a384bef53cf7996965f7cd40183",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d",
"reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d",
"shasum": ""
},
"require": {
@ -3536,7 +3579,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.20.0"
"source": "https://github.com/twigphp/Twig/tree/v3.21.1"
},
"funding": [
{
@ -3548,20 +3591,20 @@
"type": "tidelift"
}
],
"time": "2025-02-13T08:34:43+00:00"
"time": "2025-05-03T07:21:55+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.1",
"version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
@ -3620,7 +3663,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
@ -3632,22 +3675,22 @@
"type": "tidelift"
}
],
"time": "2024-07-20T21:52:34+00:00"
"time": "2025-04-30T23:37:27+00:00"
}
],
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "2.1.12",
"version": "2.1.14",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c"
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/96dde49e967c0c22812bcfa7bda4ff82c09f3b0c",
"reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"shasum": ""
},
"require": {
@ -3692,7 +3735,7 @@
"type": "github"
}
],
"time": "2025-04-16T13:19:18+00:00"
"time": "2025-05-02T15:32:28+00:00"
}
],
"aliases": [],

View file

@ -1,65 +0,0 @@
<?php
namespace Misuzu\JWT;
use RuntimeException;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey};
class ArrayJWKSet implements JWKSet {
/** @param array<string, JWK> $keys */
public function __construct(public private(set) array $keys) {}
public function getKey(?string $keyId = null, ?string $algo = null): JWK {
if($keyId === null) {
if($algo === null)
return $this->keys[array_key_first($this->keys)];
foreach($this->keys as $key)
if($key->algo !== null && hash_equals($key->algo, $algo))
return $key;
throw new RuntimeException('could not find a key that matched the requested algorithm');
}
if(!array_key_exists($keyId, $this->keys))
throw new RuntimeException('could not find a key with that id');
$key = $this->keys[$keyId];
if($algo !== null && $key->algo !== null && !hash_equals($key->algo, $algo))
throw new RuntimeException('requested algorithm does not match');
return $key;
}
/**
* @param string|mixed[]|object $json
*/
public static function decodeJson(string|array|object $json): ArrayJWKSet {
if(is_object($json))
$json = (array)$json;
elseif(is_string($json))
$json = json_decode($json);
if(is_array($json))
$json = (object)$json;
if(!property_exists($json, 'keys') || !is_array($json->keys) || !array_is_list($json->keys))
return new ArrayJWKSet([]);
$keys = [];
foreach($json->keys as $keyInfo) {
$key = PublicKeyLoader::load(json_encode($keyInfo));
if($key instanceof AsymmetricKey && ($key instanceof PrivateKey || $key instanceof PublicKey))
$key = new SecLibJWK($key, property_exists($keyInfo, 'alg') && is_string($keyInfo->alg) ? $keyInfo->alg : null);
if(property_exists($keyInfo, 'kid') && is_string($keyInfo->kid))
$keys[$keyInfo->kid] = $key;
else
$keys[] = $key;
}
return new ArrayJWKSet($keys);
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Misuzu\JWT;
use InvalidArgumentException;
class HMACJWK implements JWK {
/** @var array<string, string> */
private static array $algos = [];
public function __construct(
#[\SensitiveParameter] private string $key,
public private(set) ?string $algo = null,
public private(set) ?string $id = null,
) {}
public function sign(string $data, ?string $algo = null): string {
if($algo === null) {
if($this->algo === null)
throw new InvalidArgumentException('this key does not specify a default algorithm, you must specify $algo');
$algo = $this->algo;
}
if(!array_key_exists($algo, self::$algos))
throw new InvalidArgumentException('$algo is not a supported algorithm');
return hash_hmac(self::$algos[$algo], $data, $this->key, true);
}
public function verify(string $data, string $signature, ?string $algo = null): bool {
return hash_equals($this->sign($data, $algo), $signature);
}
/** @return string[] */
public static function algos(): array {
return array_keys(self::$algos);
}
public static function defineAlgo(string $alias, string $algo): void {
if(array_key_exists($alias, self::$algos))
throw new InvalidArgumentException('$alias has already been defined');
if(!in_array($algo, hash_hmac_algos()))
throw new InvalidArgumentException('$algo is not a supported HMAC hashing algorithm');
self::$algos[$alias] = $algo;
}
public static function init(): void {
self::defineAlgo('HS256', 'sha256');
self::defineAlgo('HS384', 'sha384');
self::defineAlgo('HS512', 'sha512');
}
}
HMACJWK::init();

View file

@ -1,13 +0,0 @@
<?php
namespace Misuzu\JWT;
use RuntimeException;
interface JWK {
public ?string $id { get; }
public ?string $algo { get; }
/** @throws RuntimeException if this instance can only be used for verification */
public function sign(string $data, ?string $algo = null): string;
public function verify(string $data, string $signature, ?string $algo = null): bool;
}

View file

@ -1,13 +0,0 @@
<?php
namespace Misuzu\JWT;
use Countable;
use RuntimeException;
interface JWKSet {
/** @var array<string, JWK> */
public array $keys { get; }
/** @throws RuntimeException if no match could be found */
public function getKey(?string $keyId = null, ?string $algo = null): JWK;
}

View file

@ -1,98 +0,0 @@
<?php
namespace Misuzu\JWT;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
use Index\{UriBase64,XDateTime};
// based on https://github.com/firebase/php-jwt/blob/8f718f4dfc9c5d5f0c994cdfd103921b43592712/src/JWT.php
class JWT {
/**
* @param array<string, mixed>|object $payload
* @param array<string, mixed>|object|null $headers
*/
public static function encode(
array|object $payload,
JWK $key,
?string $alg = null,
array|object|null $headers = null
): string {
$head = ['typ' => 'JWT'];
if($headers !== null)
$head = array_merge($head, (array)$headers);
$head['alg'] = $alg ?? $key->algo;
if($head['alg'] === null)
throw new InvalidArgumentException('$key does not provide a default algorithm, $alg must be specified');
if($key->id !== null)
$head['kid'] = $key->id;
$encoded = sprintf('%s.%s', UriBase64::encode(json_encode($head)), UriBase64::encode(json_encode($payload)));
$encoded = sprintf('%s.%s', $encoded, UriBase64::encode($key->sign($encoded, $alg)));
return $encoded;
}
/**
* @param array<string, mixed>|object|null $headers
* @return array<string, mixed>|object
*/
public static function decode(
string $token,
JWKSet $keys,
array|object|null $headers = null,
?int $timestamp = null,
int $leeway = 0,
bool $asObject = true
): array|object {
$timestamp ??= time();
$parts = explode('.', $token, 4);
if(count($parts) !== 3)
throw new InvalidArgumentException('wrong amount of segments');
[$headerEnc, $payloadEnc, $sigEnc] = $parts;
$headerRaw = UriBase64::decode($headerEnc);
$header = json_decode($headerRaw, true);
if(empty($header))
throw new UnexpectedValueException('unexpected header encoding');
if($headers !== null)
$header = (array)$headers;
$payloadRaw = UriBase64::decode($payloadEnc);
$payload = json_decode($payloadRaw, true);
if(empty($payload))
throw new UnexpectedValueException('unexpected payload encoding');
if(!is_array($payload) || array_is_list($payload))
throw new UnexpectedValueException('payload must be a JSON object');
$sig = UriBase64::decode($sigEnc);
if(empty($header['alg']) || !is_string($header['alg']))
throw new UnexpectedValueException('algorithm is not set');
$key = $keys->getKey(
array_key_exists('kid', $header) && is_string($header['kid']) ? $header['kid'] : null,
$header['alg']
);
if(!$key->verify(sprintf('%s.%s', $headerEnc, $payloadEnc), $sig, $header['alg']))
throw new RuntimeException('signature verification failed');
if(array_key_exists('nbf', $payload) && (is_int($payload['nbf']) || is_float($payload['nbf']))) {
if(floor($payload['nbf']) > ($timestamp + $leeway))
throw new RuntimeException(sprintf('token is not valid before %s', XDateTime::toIso8601String((int)floor($payload['nbf']))));
} elseif(array_key_exists('iat', $payload) && (is_int($payload['iat']) || is_float($payload['iat']))) {
if(floor($payload['iat']) > ($timestamp + $leeway))
throw new RuntimeException(sprintf('token is not valid before %s', XDateTime::toIso8601String((int)floor($payload['iat']))));
}
if(array_key_exists('exp', $payload) && (is_int($payload['exp']) || is_float($payload['exp']))
&& $payload['exp'] < ($timestamp + $leeway))
throw new RuntimeException('expired token');
if($asObject)
$payload = (object)$payload;
return $payload;
}
}

View file

@ -1,53 +0,0 @@
<?php
namespace Misuzu\JWT;
use InvalidArgumentException;
use RuntimeException;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey};
class SecLibJWK implements JWK {
public private(set) ?SecLibJWKAlgo $algoInfo;
public ?string $algo {
get => $this->algoInfo?->algo;
}
public function __construct(
#[\SensitiveParameter] private (AsymmetricKey&PrivateKey)|(AsymmetricKey&PublicKey) $key,
SecLibJWKAlgo|string|null $algo = null,
public private(set) ?string $id = null,
) {
if(is_string($algo))
$algo = SecLibJWKAlgo::create($algo);
$this->algoInfo = $algo;
}
private function resolveAlgo(SecLibJWKAlgo|string|null $algo): SecLibJWKAlgo {
if($algo === null) {
if($this->algoInfo === null)
throw new InvalidArgumentException('this key does not specify a default algorithm, you must specify $algo');
$algo = $this->algoInfo;
} elseif(is_string($algo))
$algo = SecLibJWKAlgo::create($algo);
return $algo;
}
public function sign(string $data, SecLibJWKAlgo|string|null $algo = null): string {
$key = $this->resolveAlgo($algo)->transform($this->key);
if(!($key instanceof PrivateKey))
throw new RuntimeException('this instance does not have a private key, it can only be used to verify');
return $key->sign($data);
}
public function verify(string $data, string $signature, SecLibJWKAlgo|string|null $algo = null): bool {
$key = $this->resolveAlgo($algo)->transform($this->key);
if($key instanceof PrivateKey)
$key = $key->getPublicKey();
return $key->verify($data, $signature);
}
}

View file

@ -1,165 +0,0 @@
<?php
namespace Misuzu\JWT;
use InvalidArgumentException;
use phpseclib3\Crypt\{EC,RSA};
use phpseclib3\Crypt\Common\AsymmetricKey;
class SecLibJWKAlgo {
/** @var array<string, callable(AsymmetricKey): AsymmetricKey> */
private static array $algos = [];
/** @param callable(AsymmetricKey): AsymmetricKey $transformFunc */
public function __construct(
public private(set) string $algo,
private $transformFunc
) {
if(!is_callable($transformFunc))
throw new InvalidArgumentException('$transformFunc must be callable');
}
public function transform(AsymmetricKey $key): AsymmetricKey {
return ($this->transformFunc)($key);
}
public static function create(string $algo): SecLibJWKAlgo {
if(!array_key_exists($algo, self::$algos))
throw new InvalidArgumentException(sprintf('$algo is not a supported algorithm: "%s"', $algo));
return new SecLibJWKAlgo($algo, self::$algos[$algo]);
}
public static function ensureRSAKey(AsymmetricKey $key): RSA {
if($key instanceof RSA)
return $key;
throw new InvalidArgumentException('$key must be an RSA key');
}
public static function applyPKCS1Padding(RSA $key): RSA {
return $key->withPadding(RSA::SIGNATURE_PKCS1);
}
public static function applyPSSPadding(RSA $key): RSA {
return $key->withPadding(RSA::SIGNATURE_PSS);
}
public static function ensureECKey(AsymmetricKey $key): EC {
if($key instanceof EC)
return $key;
throw new InvalidArgumentException('$key must be an EC key');
}
public static function applyIEEESignatureFormat(EC $key): EC {
return $key->withSignatureFormat('IEEE');
}
/** @return string[] */
public static function algos(): array {
return array_keys(self::$algos);
}
public static function transformRS256(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha256');
}
public static function transformRS384(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha384');
}
public static function transformRS512(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPKCS1Padding($key)->withHash('sha512');
}
public static function transformPS256(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformPS384(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformPS512(AsymmetricKey $key): RSA {
$key = self::ensureRSAKey($key);
return self::applyPSSPadding($key)->withHash('sha512');
}
public static function transformES256(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256r1')
throw new InvalidArgumentException('curve must be secp256r1');
return self::applyIEEESignatureFormat($key)->withHash('sha256');
}
public static function transformES256K(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp256k1')
throw new InvalidArgumentException('curve must be secp256k1');
return self::applyIEEESignatureFormat($key)->withHash('sha256');
}
public static function transformES384(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp384r1')
throw new InvalidArgumentException('curve must be secp384r1');
return self::applyIEEESignatureFormat($key)->withHash('sha384');
}
public static function transformES512(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'secp521r1')
throw new InvalidArgumentException('curve must be secp521r1');
return self::applyIEEESignatureFormat($key)->withHash('sha512');
}
public static function transformEdDSA(AsymmetricKey $key): EC {
$key = self::ensureECKey($key);
if($key->getCurve() !== 'Ed25519' && $key->getCurve() !== 'Ed448')
throw new InvalidArgumentException('curve must be Ed25519 or Ed448');
return $key;
}
/** @param callable(AsymmetricKey): AsymmetricKey $transformFunc */
public static function defineAlgo(string $algo, $transformFunc): void {
if(!is_callable($transformFunc))
throw new InvalidArgumentException('$transformFunc must be callable');
if(array_key_exists($algo, self::$algos))
throw new InvalidArgumentException('$algo has already been defined');
self::$algos[$algo] = $transformFunc;
}
public static function init(): void {
// RSxxx
self::defineAlgo('RS256', self::transformRS256(...));
self::defineAlgo('RS384', self::transformRS384(...));
self::defineAlgo('RS512', self::transformRS512(...));
// PSxxx
self::defineAlgo('PS256', self::transformPS256(...));
self::defineAlgo('PS384', self::transformPS384(...));
self::defineAlgo('PS512', self::transformPS512(...));
// ESxxxy
self::defineAlgo('ES256', self::transformES256(...));
self::defineAlgo('ES256K', self::transformES256K(...));
self::defineAlgo('ES384', self::transformES384(...));
self::defineAlgo('ES512', self::transformES512(...));
// EdDSA
self::defineAlgo('EdDSA', self::transformEdDSA(...));
}
}
SecLibJWKAlgo::init();

View file

@ -149,8 +149,8 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
#[ExactRoute('GET', '/.well-known/openid-configuration')]
public function getWellKnown(HttpResponseBuilder $response, HttpRequest $request): array {
$signingAlgs = array_values(array_unique(XArray::select(
$this->oauth2Ctx->keys->getPublicKeySet()->keys,
fn($key) => $key->algo
$this->oauth2Ctx->keys->getPublicKeySet()->getKeys(),
fn($key) => $key->getAlgorithm()
)));
sort($signingAlgs);

View file

@ -7,8 +7,8 @@ use Index\Db\DbConnection;
use Index\Snowflake\RandomSnowflake;
use Misuzu\SiteInfo;
use Misuzu\Apps\{AppsContext,AppInfo};
use Misuzu\JWT\JWT;
use Misuzu\Users\UserInfo;
use Railgun\Jwt\Jwt;
class OAuth2Context {
public private(set) OAuth2AuthorisationData $authorisations;
@ -167,7 +167,11 @@ class OAuth2Context {
?int $issuedAt = null,
?string $keyId = null
): string {
return JWT::encode(
$headers = null;
if($keyId !== null)
$headers = ['kid' => $keyId];
return Jwt::encode(
self::getDataForIdToken(
$this->siteInfo,
$appInfo,
@ -175,7 +179,8 @@ class OAuth2Context {
$nonce,
$issuedAt
),
$this->keys->getPrivateKeySet()->getKey($keyId)
$headers,
['keys' => $this->keys->getPrivateKeySet()],
);
}

View file

@ -5,9 +5,10 @@ use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Misuzu\Misuzu;
use Misuzu\JWT\{ArrayJWKSet,JWKSet,SecLibJWK};
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\Common\{AsymmetricKey,PrivateKey,PublicKey};
use Railgun\Jwt\{ArrayJwkSet,JwkSet};
use Railgun\Jwt\SecLib\SecLibJwk;
class OAuth2Keys {
private const string FORMAT = '%s/keys/%s-%s.pem';
@ -61,28 +62,28 @@ class OAuth2Keys {
return $key;
}
public function getPublicKeySet(): JWKSet {
public function getPublicKeySet(): JwkSet {
$keys = [];
foreach($this->keys as $keyInfo)
$keys[$keyInfo->id] = new SecLibJWK(
$keys[$keyInfo->id] = new SecLibJwk(
$this->getPublicKey($keyInfo->id),
$keyInfo->alg,
$keyInfo->id
);
return new ArrayJWKSet($keys);
return new ArrayJwkSet($keys);
}
public function getPrivateKeySet(): JWKSet {
public function getPrivateKeySet(): JwkSet {
$keys = [];
foreach($this->keys as $keyInfo)
$keys[$keyInfo->id] = new SecLibJWK(
$keys[$keyInfo->id] = new SecLibJwk(
$this->getPrivateKey($keyInfo->id),
$keyInfo->alg,
$keyInfo->id
);
return new ArrayJWKSet($keys);
return new ArrayJwkSet($keys);
}
/** @return array{keys?: array<string, string>} */