diff --git a/README.md b/README.md
index e4152cb..a12e7a4 100644
--- a/README.md
+++ b/README.md
@@ -127,17 +127,26 @@ $app->add(new Tuupola\Middleware\JwtAuthentication([
### Algorithm
-You can set supported algorithms via `algorithm` parameter. This can be either string or array of strings. Default value is `["HS256", "HS512", "HS384"]`. Supported algorithms are `HS256`, `HS384`, `HS512` and `RS256`. Note that enabling both `HS256` and `RS256` is a [security risk](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/).
+You can set supported algorithms via `algorithm` parameter. This can be either string or array of strings. Default value is `["HS256"]`. Supported algorithms are `HS256`, `HS384`, `HS512` and `RS256`. Note that enabling both `HS256` and `RS256` is a [security risk](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/).
+
+When passing multiple algorithm it be a key value array, with the key being the `KID` of the jwt.
``` php
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
- "algorithm" => ["HS256", "HS384"]
+ "algorithm" => [
+ "amce" => "HS256",
+ "beta" => "HS384"
+ ]
]));
```
+> :warning: **Warning**:
+Because of changes in `firebase/php-jwt` the `kid` is now checked when multiple algorithm are passed, failing to provide a key the algorithm will be used for the kid.
+this also means the `kid` will now need to be present in the JWT header as well.
+
### Attribute
When the token is decoded successfully and authentication succeeds the contents of the decoded token is saved as `token` attribute to the `$request` object. You can change this with. `attribute` parameter. Set to `null` or `false` to disable this behavour
diff --git a/composer.json b/composer.json
index 490a448..e25f63c 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,7 @@
"require": {
"php": "^7.4|^8.0",
"psr/log": "^1.0|^2.0|^3.0",
- "firebase/php-jwt": "^3.0|^4.0|^5.0",
+ "firebase/php-jwt": "^6.0",
"psr/http-message": "^1.0|^2.0",
"tuupola/http-factory": "^1.3",
"tuupola/callable-handler": "^1.0",
diff --git a/src/JwtAuthentication.php b/src/JwtAuthentication.php
index 6a453bd..e0dab4c 100644
--- a/src/JwtAuthentication.php
+++ b/src/JwtAuthentication.php
@@ -34,13 +34,15 @@
namespace Tuupola\Middleware;
+use ArrayAccess;
use Closure;
use DomainException;
-use InvalidArgumentException;
use Exception;
use Firebase\JWT\JWT;
-use Psr\Http\Message\ServerRequestInterface;
+use Firebase\JWT\Key;
+use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
@@ -52,6 +54,11 @@
use Tuupola\Middleware\JwtAuthentication\RequestPathRule;
use Tuupola\Middleware\JwtAuthentication\RuleInterface;
+use function array_fill_keys;
+use function array_keys;
+use function count;
+use function is_array;
+
final class JwtAuthentication implements MiddlewareInterface
{
use DoublePassTrait;
@@ -72,10 +79,10 @@ final class JwtAuthentication implements MiddlewareInterface
* Stores all the options passed to the middleware.
*
* @var array{
- * secret?: string|array,
+ * secret?: string|array|array,
* secure: bool,
* relaxed: array,
- * algorithm: array,
+ * algorithm: array|array,
* header: string,
* regexp: string,
* cookie: string,
@@ -90,7 +97,7 @@ final class JwtAuthentication implements MiddlewareInterface
private array $options = [
"secure" => true,
"relaxed" => ["localhost", "127.0.0.1"],
- "algorithm" => ["HS256", "HS512", "HS384"],
+ "algorithm" => ["HS256"],
"header" => "Authorization",
"regexp" => "/Bearer\s+(.*)$/i",
"cookie" => "token",
@@ -306,11 +313,15 @@ private function fetchToken(ServerRequestInterface $request): string
*/
private function decodeToken(string $token): array
{
+ $keys = $this->createKeysFromAlgorithms();
+ if (count($keys) === 1) {
+ $keys = current($keys);
+ }
+
try {
$decoded = JWT::decode(
$token,
- $this->options["secret"],
- (array) $this->options["algorithm"]
+ $keys,
);
return (array) $decoded;
} catch (Exception $exception) {
@@ -326,6 +337,16 @@ private function decodeToken(string $token): array
*/
private function hydrate(array $data = []): void
{
+ $data['algorithm'] = $data['algorithm'] ?? $this->options['algorithm'];
+ if ((is_array($data['secret']) || $data['secret'] instanceof ArrayAccess)
+ && is_array($data['algorithm'])
+ && count($data['algorithm']) === 1
+ && count((array) $data['secret']) > count($data['algorithm'])
+ ) {
+ $secretIndex = array_keys((array) $data['secret']);
+ $data['algorithm'] = array_fill_keys($secretIndex, $data['algorithm'][0]);
+ }
+
foreach ($data as $key => $value) {
/* https://github.com/facebook/hhvm/issues/6368 */
$key = str_replace(".", " ", $key);
@@ -504,4 +525,31 @@ private function rules(array $rules): void
$this->rules->push($callable);
}
}
+
+ /**
+ * @return array
+ */
+ private function createKeysFromAlgorithms(): array
+ {
+ if (!isset($this->options["secret"])) {
+ throw new InvalidArgumentException(
+ 'Secret must be either a string or an array of "kid" => "secret" pairs'
+ );
+ }
+
+ $keyObjects = [];
+ foreach ($this->options["algorithm"] as $kid => $algorithm) {
+ $keyId = !is_numeric($kid) ? $kid : $algorithm;
+
+ $secret = $this->options["secret"];
+
+ if (is_array($secret) || $secret instanceof ArrayAccess) {
+ $secret = $this->options["secret"][$kid];
+ }
+
+ $keyObjects[$keyId] = new Key($secret, $algorithm);
+ }
+
+ return $keyObjects;
+ }
}
diff --git a/tests/ArrayAccessImpl.php b/tests/ArrayAccessImpl.php
deleted file mode 100644
index 9f3fa33..0000000
--- a/tests/ArrayAccessImpl.php
+++ /dev/null
@@ -1,58 +0,0 @@
-array[$offset]);
- }
-
- public function offsetGet($offset)
- {
- return $this->array[$offset];
- }
-
- public function offsetSet($offset, $value)
- {
- $this->array[$offset] = $value;
- }
-
- public function offsetUnset($offset)
- {
- unset($this->array[$offset]);
- }
-}
diff --git a/tests/JwtAuthenticationTest.php b/tests/JwtAuthenticationTest.php
index 12c704f..9f232f6 100644
--- a/tests/JwtAuthenticationTest.php
+++ b/tests/JwtAuthenticationTest.php
@@ -32,6 +32,7 @@
namespace Tuupola\Middleware;
+use ArrayObject;
use Equip\Dispatch\MiddlewareCollection;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
@@ -210,7 +211,34 @@ public function testShouldReturn200WithSecretArray(): void
"secret" => [
"acme" =>"supersecretkeyyoushouldnotcommittogithub",
"beta" =>"anothersecretkeyfornevertocommittogithub"
- ]
+ ],
+ ])
+ ]);
+
+ $response = $collection->dispatch($request, $default);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals("Success", $response->getBody());
+ }
+
+ public function testShouldReturn200WithSecretArrayCheckKid(): void
+ {
+ $request = (new ServerRequestFactory)
+ ->createServerRequest("GET", "https://example.com/api")
+ ->withHeader("Authorization", "Bearer " . self::$betaToken);
+
+ $default = function (ServerRequestInterface $request) {
+ $response = (new ResponseFactory)->createResponse();
+ $response->getBody()->write("Success");
+ return $response;
+ };
+
+ $collection = new MiddlewareCollection([
+ new JwtAuthentication([
+ "algorithm" => ["acme" => "HS256", "beta" => "HS256"],
+ "secret" => [
+ "acme" =>"supersecretkeyyoushouldnotcommittogithub",
+ "beta" =>"anothersecretkeyfornevertocommittogithub"
+ ],
])
]);
@@ -257,13 +285,13 @@ public function testShouldReturn200WithSecretArrayAccess(): void
return $response;
};
- $secret = new ArrayAccessImpl();
+ $secret = new ArrayObject();
$secret["acme"] = "supersecretkeyyoushouldnotcommittogithub";
$secret["beta"] ="anothersecretkeyfornevertocommittogithub";
$collection = new MiddlewareCollection([
new JwtAuthentication([
- "secret" => $secret
+ "secret" => $secret,
])
]);
@@ -284,13 +312,13 @@ public function testShouldReturn401WithSecretArrayAccess(): void
return $response;
};
- $secret = new ArrayAccessImpl();
+ $secret = new ArrayObject();
$secret["xxxx"] = "supersecretkeyyoushouldnotcommittogithub";
$secret["yyyy"] = "anothersecretkeyfornevertocommittogithub";
$collection = new MiddlewareCollection([
new JwtAuthentication([
- "secret" => $secret
+ "secret" => $secret,
])
]);