diff --git a/README.md b/README.md
index e4152cb..8f67643 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,47 @@
# PSR-7 and PSR-15 JWT Authentication Middleware
+> [!IMPORTANT]
+> This is a replacement for `tuupola/slim-jwt-auth` with the updated version of `firebase/php-jwt` to resolve
+> [CVE-2021-46743](https://nvd.nist.gov/vuln/detail/CVE-2021-46743) for the meantime I plan to maintiane conpatability in v1 some,
+> there is v2 I plan to deverge
This middleware implements JSON Web Token Authentication. It was originally developed for Slim but can be used with any framework using PSR-7 and PSR-15 style middlewares. It has been tested with [Slim Framework](http://www.slimframework.com/) and [Zend Expressive](https://zendframework.github.io/zend-expressive/).
-[![Latest Version](https://img.shields.io/packagist/v/tuupola/slim-jwt-auth.svg?style=flat-square)](https://packagist.org/packages/tuupola/slim-jwt-auth)
-[![Packagist](https://img.shields.io/packagist/dm/tuupola/slim-jwt-auth.svg)](https://packagist.org/packages/tuupola/slim-jwt-auth)
+[![Latest Version](https://img.shields.io/packagist/v/jimtools/jwt-auth.svg?style=flat-square)](https://packagist.org/packages/jimtools/jwt-auth)
+[![Packagist](https://img.shields.io/packagist/dm/jimtools/jwt-auth.svg?style=flat-square)](https://packagist.org/packages/jimtools/jwt-auth)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
-[![Build Status](https://img.shields.io/github/actions/workflow/status/tuupola/slim-jwt-auth/tests.yml?branch=3.x&style=flat-square)](https://github.com/tuupola/slim-jwt-auth/actions)
-[![Coverage](https://img.shields.io/codecov/c/github/tuupola/slim-jwt-auth/3.x.svg?style=flat-square)](https://codecov.io/github/tuupola/slim-jwt-auth/branch/3.x)
+[![Build Status](https://img.shields.io/github/actions/workflow/status/jimtools/jwt-auth/tests.yml?branch=main&style=flat-square)](https://github.com/jimtools/jwt-auth/actions)
+[![Coverage](https://img.shields.io/codecov/c/gh/jimtools/jwt-auth/main.svg?style=flat-square)](https://codecov.io/github/jimtools/jwt-auth/branch/main)
-Heads up! You are reading documentation for [3.x branch](https://github.com/tuupola/slim-jwt-auth/tree/3.x) which is PHP 7.1 and up only. If you are using older version of PHP see the [2.x branch](https://github.com/tuupola/slim-jwt-auth/tree/2.x). These two branches are not backwards compatible, see [UPGRADING](https://github.com/tuupola/slim-jwt-auth/blob/3.x/UPGRADING.md) for instructions how to upgrade.
+
+Heads up! You are reading documentation for [3.x branch](https://github.com/tuupola/slim-jwt-auth/tree/3.x) which is PHP 7.4 and up only. If you are using older version of PHP see the [2.x branch](https://github.com/tuupola/slim-jwt-auth/tree/2.x). These two branches are not backwards compatible, see [UPGRADING](https://github.com/tuupola/slim-jwt-auth/blob/main/UPGRADING.md) for instructions how to upgrade.
Middleware does **not** implement OAuth 2.0 authorization server nor does it provide ways to generate, issue or store authentication tokens. It only parses and authenticates a token when passed via header or cookie. This is useful for example when you want to use [JSON Web Tokens as API keys](https://auth0.com/blog/2014/12/02/using-json-web-tokens-as-api-keys/).
For example implementation see [Slim API Skeleton](https://github.com/tuupola/slim-api-skeleton).
+## Breaking Changes
+Because of the way firebase/php-jwt:v6 now works, the way `secrets` and `algorithm` are pass needs to change so the following change will need to be made.
+
+```php
+$app = new Slim\App;
+
+$app->add(new Tuupola\Middleware\JwtAuthentication([
+ "secret" => [
+ "acme" => "supersecretkeyyoushouldnotcommittogithub",
+ "beta" => "supersecretkeyyoushouldnotcommittogithub",
+ "algorithm" => [
+ "amce" => "HS256",
+ "beta" => "HS384"
+ ]
+]));
+```
+
## Install
Install latest version using [composer](https://getcomposer.org/).
``` bash
-$ composer require tuupola/slim-jwt-auth
+$ composer require jimtools/jwt-auth
```
If using Apache add the following to the `.htaccess` file. Otherwise PHP wont have access to `Authorization: Bearer` header.
@@ -127,17 +149,28 @@ $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"]
+ "secret" => [
+ "acme" => "supersecretkeyyoushouldnotcommittogithub",
+ "beta" => "supersecretkeyyoushouldnotcommittogithub",
+ "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..057d936 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
- "name": "tuupola/slim-jwt-auth",
- "description": "PSR-7 and PSR-15 JWT Authentication Middleware",
+ "name": "jimtools/jwt-auth",
+ "description": "Drop in replacement for tuupola/slim-jwt-auth",
"keywords": [
"psr-7",
"psr-15",
@@ -9,20 +9,20 @@
"json",
"auth"
],
- "homepage": "https://github.com/tuupola/slim-jwt-auth",
+ "homepage": "https://github.com/jimtools/jwt-auth",
"license": "MIT",
"authors": [
{
- "name": "Mika Tuupola",
- "email": "tuupola@appelsiini.net",
- "homepage": "https://appelsiini.net/",
+ "name": "James Read",
+ "email": "james.read.18@gmail.com",
+ "homepage": "https://github.com/jimtools",
"role": "Developer"
}
],
"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",
@@ -50,5 +50,8 @@
"branch-alias": {
"dev-3.x": "3.0.x-dev"
}
+ },
+ "conflict": {
+ "tuupola/slim-jwt-auth": "*"
}
}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index c88bd3a..8c455de 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,24 +1,82 @@
parameters:
ignoreErrors:
-
- message: '#^Method .* is unused.$#'
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:after\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
-
- message: "#^Offset 'secret' does not exist on array\\{secret\\?\\: array\\\\|string, secure\\: bool, relaxed\\: array\\, algorithm\\: array\\, header\\: string, regexp\\: string, cookie\\: string, attribute\\: string, \\.\\.\\.\\}\\.$#"
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:algorithm\\(\\) is unused\\.$#"
count: 1
path: src/JwtAuthentication.php
-
- message: "#^Parameter \\#1 \\$callback of function call_user_func expects callable\\(\\)\\: mixed, array\\{\\$this\\(Tuupola\\\\Middleware\\\\JwtAuthentication\\), string\\} given\\.$#"
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:attribute\\(\\) is unused\\.$#"
count: 1
path: src/JwtAuthentication.php
-
- message: "#^Parameter \\#1 \\$value of method SplDoublyLinkedList\\\\:\\:push\\(\\) expects Tuupola\\\\Middleware\\\\JwtAuthentication\\\\RuleInterface, callable\\(\\)\\: mixed given\\.$#"
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:before\\(\\) is unused\\.$#"
count: 1
path: src/JwtAuthentication.php
-
- message: "#^Property Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:\\$message is unused\\.$#"
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:cookie\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:error\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:header\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:ignore\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:logger\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:path\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:regexp\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:relaxed\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:rules\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:secret\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Method Tuupola\\\\Middleware\\\\JwtAuthentication\\:\\:secure\\(\\) is unused\\.$#"
+ count: 1
+ path: src/JwtAuthentication.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of method SplDoublyLinkedList\\\\:\\:push\\(\\) expects Tuupola\\\\Middleware\\\\JwtAuthentication\\\\RuleInterface, callable\\(\\)\\: mixed given\\.$#"
count: 1
path: src/JwtAuthentication.php
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,
])
]);