Skip to content

Commit

Permalink
Merge pull request #51 from bizley/v4
Browse files Browse the repository at this point in the history
v4
  • Loading branch information
Bizley authored Feb 26, 2023
2 parents c764936 + 3da68b1 commit 81a92a6
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 321 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0', '8.1']
php: ['8.1', '8.2']

steps:
- name: Checkout
Expand Down
3 changes: 3 additions & 0 deletions INSTRUCTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ Validation constraints used here are:
- `LooseValidAt` - this will make sure that token is not expired yet allowing 10 seconds leeway (in case of some delays
between the server and the client), we are here also setting the same time zone that is used in the application.

*NOTE*: The above implementation requires to install `lcobucci/clock` library first (run `composer req lcobucci/clock`).
If you prefer other PSR-20 clock implementation you must change the above `\Lcobucci\Clock\SystemClock()` usage.

You can also add here any other constraint that you find necessary. The available list is at
https://github.com/lcobucci/jwt/tree/4.1.x/src/Validation/Constraint, and you can always write your own constraint as
long as it implements `Lcobucci\JWT\Validation\Constraint`.
Expand Down
36 changes: 13 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ This extension provides the [JWT](https://github.com/lcobucci/jwt) integration f

> This is a fork of [sizeg/yii2-jwt](https://github.com/sizeg/yii2-jwt) package
**Version 3.x of this package uses `lcobucci/jwt` [v4](https://github.com/lcobucci/jwt/releases/tag/4.0.0)
and introduces critical BC changes, [see v4 lcobucci/jwt Upgrade Guide](https://lcobucci-jwt.readthedocs.io/en/latest/upgrading/).
For 2.x (and `lcobucci/jwt` v3) install `^2.0`.**
# Available versions

| bizley/yii2-jwt | lcobucci/jwt | php |
|:---------------:|:------------:|:-------:|
| `^4.0` | `^5.0` | `>=8.1` |
| `^3.0` | `^4.0` | `>=7.4` |
| `^2.0` | `^3.0` | `>=7.1` |

See [lcobucci/jwt](https://github.com/lcobucci/jwt) repo for details about the version.

## Installation

Expand All @@ -20,12 +26,12 @@ Add the package to your `composer.json`:
```json
{
"require": {
"bizley/jwt": "^3.0"
"bizley/jwt": "^4.0"
}
}
```

and run `composer update` or alternatively run `composer require bizley/jwt:^3.0`
and run `composer update` or alternatively run `composer require bizley/jwt:^4.0`

## Basic usage

Expand Down Expand Up @@ -68,17 +74,7 @@ and `algorithmTypes` and using its ID for `signer`.
### Note on signers and minimum bits requirement

Since `lcobucci/jwt 4.2.0` signers require the minimum key length to make sure those are properly secured, otherwise
the `InvalidKeyProvided` is thrown. If for any reason (**and on your own risk**) you would still like to use the less
secure key (for example HS256 with fewer than 256 bits length) you can wire it through this library by using the
`Unsafe` version of that signer (for example `Lcobucci\JWT\Signer\Hmac\Sha256` has the unsafe version
`Lcobucci\JWT\Signer\Hmac\UnsafeSha256`). Unsafe versions are using the same algorithm ID, so you don't have to add them
on the `Jwt::$algorithmTypes` list, but you need to configure them manually for your signer configuration like:

```php
[
'signer' => [\Lcobucci\JWT\Signer\Hmac\UnsafeSha256::class],
]
```
the `InvalidKeyProvided` is thrown.

### Keys

Expand All @@ -91,16 +87,12 @@ Configuration array can be as the following:
[
'key' => /* key content */,
'passphrase' => /* key passphrase */,
'store' => /* storage type */,
'method' => /* method type */,
]
```

- key (Jwt::KEY) - _string_, default `''`,
- passphrase (Jwt::PASSPHRASE) - _string_, default `''`,
- store (Jwt::STORE) - _string_, default `Jwt::STORE_IN_MEMORY`,
available: `Jwt::STORE_IN_MEMORY`, `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated since 3.2.0, will be removed in 4.0.0)
(see https://lcobucci-jwt.readthedocs.io/en/latest/configuration/)
- method (Jwt::METHOD) - _string_, default `Jwt::METHOD_PLAIN`,
available: `Jwt::METHOD_PLAIN`, `Jwt::METHOD_BASE64`, `Jwt::METHOD_FILE`
(see https://lcobucci-jwt.readthedocs.io/en/latest/configuration/)
Expand All @@ -111,18 +103,16 @@ Simple string keys are shortcuts to the following array configs:
[
'key' => /* given key itself */,
'passphrase' => '',
'store' => Jwt::STORE_IN_MEMORY,
'method' => Jwt::METHOD_FILE,
]
```
Detecting `@` at the beginning assumes Yii alias has been provided so it will be resolved with `Yii::getAlias()`.
Detecting `@` at the beginning assumes Yii alias has been provided, so it will be resolved with `Yii::getAlias()`.

- key doesn't start with `@` nor `file://`:
```php
[
'key' => /* given key itself */,
'passphrase' => '',
'store' => Jwt::STORE_IN_MEMORY,
'method' => Jwt::METHOD_PLAIN,
]
```
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
"source": "https://github.com/bizley/yii2-jwt"
},
"require": {
"php": ">=7.4",
"lcobucci/jwt": "^4.1",
"php": ">=8.1",
"lcobucci/jwt": "^5.0",
"yiisoft/yii2": ">=2.0.14 <2.1"
},
"require-dev": {
"infection/infection": "*",
"lcobucci/clock": "^3.0",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^9.3",
"phpunit/phpunit": "^9.6",
"roave/security-advisories": "dev-latest"
},
"autoload": {
Expand Down
2 changes: 1 addition & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@default": true,
"MethodCallRemoval": {
"ignore": [
"bizley\\jwt\\Jwt::init::193",
"bizley\\jwt\\Jwt::init::186",
"bizley\\jwt\\JwtHttpBearerAuth::init::77"
]
}
Expand Down
111 changes: 42 additions & 69 deletions src/Jwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Lcobucci\JWT\Decoder;
use Lcobucci\JWT\Encoder;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Token;
Expand All @@ -26,10 +27,10 @@
use function is_callable;
use function is_string;
use function reset;
use function strpos;
use function str_starts_with;

/**
* JSON Web Token implementation based on lcobucci/jwt library v4.
* JSON Web Token implementation based on lcobucci/jwt library v5.
* @see https://github.com/lcobucci/jwt
*
* @author Paweł Bizley Brzozowski <[email protected]> since 2.0 (fork)
Expand All @@ -50,7 +51,6 @@ class Jwt extends Component
public const BLAKE2B = 'BLAKE2B';

public const STORE_IN_MEMORY = 'in_memory';
public const STORE_LOCAL_FILE_REFERENCE = 'local_file_reference'; // deprecated since 3.2.0, will be removed in 4.0.0

public const METHOD_PLAIN = 'plain';
public const METHOD_BASE64 = 'base64';
Expand All @@ -69,26 +69,20 @@ class Jwt extends Component
* This can be a simple string, an instance of Key, or a configuration array.
* The configuration takes the following array keys:
* - 'key' => Key's value or path to the key file.
* - 'store' => Either `Jwt::STORE_IN_MEMORY` or `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated) -
* whether to keep the key in the memory or as a reference to a local file.
* - 'method' => `Jwt::METHOD_PLAIN`, `Jwt::METHOD_BASE64`, or `Jwt::METHOD_FILE` - whether the key is a plain
* text, base64 encoded text, or a file.
* In case the 'store' is set to `Jwt::STORE_LOCAL_FILE_REFERENCE` (deprecated), only
* `Jwt::METHOD_FILE` method is available.
* - 'passphrase' => Key's passphrase.
* In case a simple string is provided (and it does not start with 'file://' or '@') the following configuration
* is assumed:
* [
* 'key' => // the original given value,
* 'store' => Jwt::STORE_IN_MEMORY,
* 'method' => Jwt::METHOD_PLAIN,
* 'passphrase' => '',
* ]
* In case a simple string is provided and it does start with 'file://' (direct file path) or '@' (Yii alias)
* the following configuration is assumed:
* [
* 'key' => // the original given value,
* 'store' => Jwt::STORE_IN_MEMORY,
* 'method' => Jwt::METHOD_FILE,
* 'passphrase' => '',
* ]
Expand All @@ -107,12 +101,11 @@ class Jwt extends Component
public $verifyingKey = '';

/**
* @var string|Signer|null Signer ID or Signer instance to be used for signing/verifying.
* See $signers for available values. In case it's not set, no algorithm will be used, which may be handy if you
* want to do some testing, but it's NOT recommended for production environments.
* @var string|Signer Signer ID or Signer instance to be used for signing/verifying.
* See $signers for available values. Since 4.0.0 it cannot be empty anymore.
* @since 3.0.0
*/
public $signer;
public $signer = '';

/**
* @var array<string, string[]> Default signers configuration. When instantiated it will use selected array to
Expand Down Expand Up @@ -192,31 +185,27 @@ public function init(): void
{
parent::init();

if ($this->signer === null) {
$this->configuration = Configuration::forUnsecuredSigner($this->prepareEncoder(), $this->prepareDecoder());
$signerId = $this->signer;
if ($this->signer instanceof Signer) {
$signerId = $this->signer->algorithmId();
}
if (in_array($signerId, $this->algorithmTypes[self::SYMMETRIC], true)) {
$this->configuration = Configuration::forSymmetricSigner(
$this->prepareSigner($this->signer),
$this->prepareKey($this->signingKey),
$this->prepareEncoder(),
$this->prepareDecoder()
);
} elseif (in_array($signerId, $this->algorithmTypes[self::ASYMMETRIC], true)) {
$this->configuration = Configuration::forAsymmetricSigner(
$this->prepareSigner($this->signer),
$this->prepareKey($this->signingKey),
$this->prepareKey($this->verifyingKey),
$this->prepareEncoder(),
$this->prepareDecoder()
);
} else {
$signerId = $this->signer;
if ($this->signer instanceof Signer) {
$signerId = $this->signer->algorithmId();
}
if (in_array($signerId, $this->algorithmTypes[self::SYMMETRIC], true)) {
$this->configuration = Configuration::forSymmetricSigner(
$this->prepareSigner($this->signer),
$this->prepareKey($this->signingKey),
$this->prepareEncoder(),
$this->prepareDecoder()
);
} elseif (in_array($signerId, $this->algorithmTypes[self::ASYMMETRIC], true)) {
$this->configuration = Configuration::forAsymmetricSigner(
$this->prepareSigner($this->signer),
$this->prepareKey($this->signingKey),
$this->prepareKey($this->verifyingKey),
$this->prepareEncoder(),
$this->prepareDecoder()
);
} else {
throw new InvalidConfigException('Invalid signer ID!');
}
throw new InvalidConfigException('Invalid signer ID!');
}
}

Expand Down Expand Up @@ -251,6 +240,7 @@ public function getConfiguration(): Configuration

/**
* Since 3.0.0 this method is using different signature.
* Please note that since 4.0.0 Builder object is immutable.
* @see https://lcobucci-jwt.readthedocs.io/en/latest/issuing-tokens/ for details of using the builder.
* @throws InvalidConfigException
*/
Expand All @@ -270,6 +260,7 @@ public function getParser(): Parser
}

/**
* @param non-empty-string $jwt
* @throws CannotDecodeContent When something goes wrong while decoding.
* @throws Token\InvalidTokenStructure When token string structure is invalid.
* @throws Token\UnsupportedHeaderFound When parsed token has an unsupported header.
Expand All @@ -284,7 +275,7 @@ public function parse(string $jwt): Token
/**
* This method goes through every single constraint in the set, groups all the violations, and throws an exception
* with the grouped violations.
* @param string|Token $jwt JWT string or instance of Token
* @param non-empty-string|Token $jwt JWT string or instance of Token
* @throws Validation\RequiredConstraintsViolated When constraint is violated
* @throws Validation\NoConstraintsGiven When no constraints are provided
* @throws InvalidConfigException
Expand All @@ -300,7 +291,7 @@ public function assert($jwt): void

/**
* This method return false on first constraint violation
* @param string|Token $jwt JWT string or instance of Token
* @param non-empty-string|Token $jwt JWT string or instance of Token
* @throws InvalidConfigException
* @since 3.0.0
*/
Expand Down Expand Up @@ -331,22 +322,19 @@ private function prepareKey($key): Signer\Key
if ($key === '') {
throw new InvalidConfigException('Empty string used as a key configuration!');
}
if (strpos($key, '@') === 0) {
if (str_starts_with($key, '@')) {
$keyConfig = [
self::KEY => Yii::getAlias($key),
self::STORE => self::STORE_IN_MEMORY,
self::METHOD => self::METHOD_FILE,
];
} elseif (strpos($key, 'file://') === 0) {
} elseif (str_starts_with($key, 'file://')) {
$keyConfig = [
self::KEY => $key,
self::STORE => self::STORE_IN_MEMORY,
self::METHOD => self::METHOD_FILE,
];
} else {
$keyConfig = [
self::KEY => $key,
self::STORE => self::STORE_IN_MEMORY,
self::METHOD => self::METHOD_PLAIN,
];
}
Expand All @@ -357,42 +345,27 @@ private function prepareKey($key): Signer\Key
}

$value = $keyConfig[self::KEY] ?? '';
$store = $keyConfig[self::STORE] ?? self::STORE_IN_MEMORY;
$method = $keyConfig[self::METHOD] ?? self::METHOD_PLAIN;
$passphrase = $keyConfig[self::PASSPHRASE] ?? '';

if (!is_string($value)) {
if (!is_string($value) || $value === '') {
throw new InvalidConfigException('Invalid key value!');
}
if (!in_array($store, [self::STORE_IN_MEMORY, self::STORE_LOCAL_FILE_REFERENCE], true)) {
throw new InvalidConfigException('Invalid key store!');
}
if (!in_array($method, [self::METHOD_PLAIN, self::METHOD_BASE64, self::METHOD_FILE], true)) {
throw new InvalidConfigException('Invalid key method!');
}
if (!is_string($passphrase)) {
throw new InvalidConfigException('Invalid key passphrase!');
}

if ($store === self::STORE_IN_MEMORY) {
if ($value === '') {
return Signer\Key\InMemory::empty();
}
if ($method === self::METHOD_BASE64) {
return Signer\Key\InMemory::base64Encoded($value, $passphrase);
}
if ($method === self::METHOD_FILE) {
return Signer\Key\InMemory::file($value, $passphrase);
}

return Signer\Key\InMemory::plainText($value, $passphrase);
if ($method === self::METHOD_BASE64) {
return Signer\Key\InMemory::base64Encoded($value, $passphrase);
}

if ($method !== self::METHOD_FILE) {
throw new InvalidConfigException('Invalid key store and method combination!');
if ($method === self::METHOD_FILE) {
return Signer\Key\InMemory::file($value, $passphrase);
}

return Signer\Key\InMemory::file($value, $passphrase);
return Signer\Key\InMemory::plainText($value, $passphrase);
}

/**
Expand Down Expand Up @@ -454,10 +427,10 @@ private function prepareValidationConstraints(): array
/**
* @throws InvalidConfigException
*/
private function prepareEncoder(): ?Encoder
private function prepareEncoder(): Encoder
{
if ($this->encoder === null) {
return null;
return new JoseEncoder();
}

/** @var Encoder $encoder */
Expand All @@ -469,10 +442,10 @@ private function prepareEncoder(): ?Encoder
/**
* @throws InvalidConfigException
*/
private function prepareDecoder(): ?Decoder
private function prepareDecoder(): Decoder
{
if ($this->decoder === null) {
return null;
return new JoseEncoder();
}

/** @var Decoder $decoder */
Expand Down
Loading

0 comments on commit 81a92a6

Please sign in to comment.