Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Feature/conflit package #251

Closed
wants to merge 12 commits into from
51 changes: 42 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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**: <br>
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
Expand Down
17 changes: 10 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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": "[email protected]",
"homepage": "https://appelsiini.net/",
"name": "James Read",
"email": "[email protected]",
"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",
Expand Down Expand Up @@ -50,5 +50,8 @@
"branch-alias": {
"dev-3.x": "3.0.x-dev"
}
},
"conflict": {
"tuupola/slim-jwt-auth": "*"
}
}
68 changes: 63 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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\\>\\|string, secure\\: bool, relaxed\\: array\\<string\\>, algorithm\\: array\\<string\\>, 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\\<Tuupola\\\\Middleware\\\\JwtAuthentication\\\\RuleInterface\\>\\:\\: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\\<Tuupola\\\\Middleware\\\\JwtAuthentication\\\\RuleInterface\\>\\:\\:push\\(\\) expects Tuupola\\\\Middleware\\\\JwtAuthentication\\\\RuleInterface, callable\\(\\)\\: mixed given\\.$#"
count: 1
path: src/JwtAuthentication.php

Expand Down
62 changes: 55 additions & 7 deletions src/JwtAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -72,10 +79,10 @@ final class JwtAuthentication implements MiddlewareInterface
* Stores all the options passed to the middleware.
*
* @var array{
* secret?: string|array<string>,
* secret?: string|array<string>|array<string,string>,
* secure: bool,
* relaxed: array<string>,
* algorithm: array<string>,
* algorithm: array<string>|array<string,string>,
* header: string,
* regexp: string,
* cookie: string,
Expand All @@ -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",
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -504,4 +525,31 @@ private function rules(array $rules): void
$this->rules->push($callable);
}
}

/**
* @return array<string,Key>
*/
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;
}
}
58 changes: 0 additions & 58 deletions tests/ArrayAccessImpl.php

This file was deleted.

Loading
Loading