├── .gitattributes
├── .gitignore
├── .php-cs-fixer.php
├── .travis.yml
├── .travis
├── ci.ini
└── swoole.install.sh
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── publish
└── jwt.php
└── src
├── Blacklist.php
├── Claims
├── AbstractClaim.php
├── Audience.php
├── Collection.php
├── Custom.php
├── DatetimeTrait.php
├── Expiration.php
├── Factory.php
├── IssuedAt.php
├── Issuer.php
├── JwtId.php
├── NotBefore.php
└── Subject.php
├── Codec.php
├── Commands
├── AbstractGenCommand.php
├── GenJwtKeypairCommand.php
└── GenJwtSecretCommand.php
├── ConfigProvider.php
├── Contracts
├── ClaimInterface.php
├── CodecInterface.php
├── JwtFactoryInterface.php
├── JwtSubjectInterface.php
├── ManagerInterface.php
├── PayloadValidatorInterface.php
├── RequestParser
│ ├── HandlerInterface.php
│ └── RequestParserInterface.php
├── StorageInterface.php
├── TokenValidatorInterface.php
└── ValidatorInterface.php
├── CustomClaims.php
├── Exceptions
├── InvalidClaimException.php
├── InvalidConfigException.php
├── JwtException.php
├── PayloadException.php
├── TokenBlacklistedException.php
├── TokenExpiredException.php
├── TokenInvalidException.php
└── UserNotDefinedException.php
├── Jwt.php
├── JwtFactory.php
├── Manager.php
├── ManagerFactory.php
├── Payload.php
├── PayloadFactory.php
├── RequestParser
├── Handlers
│ ├── AuthHeaders.php
│ ├── Cookies.php
│ ├── InputSource.php
│ ├── KeyTrait.php
│ ├── QueryString.php
│ └── RouteParams.php
├── RequestParser.php
└── RequestParserFactory.php
├── Storage
└── HyperfCache.php
├── Token.php
├── Utils.php
└── Validators
├── PayloadValidator.php
└── TokenValidator.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests export-ignore
2 | /.github export-ignore
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | *.cache
4 | *.log
5 | .idea/
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
13 | ->setRules([
14 | '@PSR2' => true,
15 | '@Symfony' => true,
16 | '@DoctrineAnnotation' => true,
17 | '@PhpCsFixer' => true,
18 | 'header_comment' => [
19 | 'comment_type' => 'PHPDoc',
20 | 'header' => $header,
21 | 'separate' => 'none',
22 | 'location' => 'after_declare_strict',
23 | ],
24 | 'array_syntax' => [
25 | 'syntax' => 'short'
26 | ],
27 | 'list_syntax' => [
28 | 'syntax' => 'short'
29 | ],
30 | 'concat_space' => [
31 | 'spacing' => 'one'
32 | ],
33 | 'blank_line_before_statement' => [
34 | 'statements' => [
35 | 'declare',
36 | ],
37 | ],
38 | 'general_phpdoc_annotation_remove' => [
39 | 'annotations' => [
40 | 'author'
41 | ],
42 | ],
43 | 'ordered_imports' => [
44 | 'imports_order' => [
45 | 'class', 'function', 'const',
46 | ],
47 | 'sort_algorithm' => 'alpha',
48 | ],
49 | 'single_line_comment_style' => [
50 | 'comment_types' => [
51 | ],
52 | ],
53 | 'yoda_style' => [
54 | 'always_move_variable' => false,
55 | 'equal' => false,
56 | 'identical' => false,
57 | ],
58 | 'phpdoc_align' => [
59 | 'align' => 'left',
60 | ],
61 | 'multiline_whitespace_before_semicolons' => [
62 | 'strategy' => 'no_multi_line',
63 | ],
64 | 'constant_case' => [
65 | 'case' => 'lower',
66 | ],
67 | 'class_attributes_separation' => true,
68 | 'combine_consecutive_unsets' => true,
69 | 'declare_strict_types' => true,
70 | 'linebreak_after_opening_tag' => true,
71 | 'lowercase_static_reference' => true,
72 | 'no_useless_else' => true,
73 | 'no_unused_imports' => true,
74 | 'not_operator_with_successor_space' => true,
75 | 'not_operator_with_space' => false,
76 | 'ordered_class_elements' => true,
77 | 'php_unit_strict' => false,
78 | 'phpdoc_separation' => false,
79 | 'single_quote' => true,
80 | 'standardize_not_equals' => true,
81 | 'multiline_comment_opening_closing' => true,
82 | ])
83 | ->setFinder(
84 | PhpCsFixer\Finder::create()
85 | ->exclude('bin')
86 | ->exclude('public')
87 | ->exclude('runtime')
88 | ->exclude('vendor')
89 | ->in(__DIR__)
90 | )
91 | ->setUsingCache(false);
92 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: required
4 |
5 | matrix:
6 | include:
7 | - php: 7.2
8 | env: SW_VERSION="4.5.3RC1"
9 | - php: 7.3
10 | env: SW_VERSION="4.5.3RC1"
11 | - php: 7.4
12 | env: SW_VERSION="4.5.3RC1"
13 |
14 | allow_failures:
15 | - php: master
16 |
17 | services:
18 | - docker
19 |
20 | before_install:
21 | - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)"
22 | - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)"
23 | - echo $PHP_MAJOR
24 | - echo $PHP_MINOR
25 |
26 | install:
27 | - cd $TRAVIS_BUILD_DIR
28 | - bash .travis/swoole.install.sh
29 | - phpenv config-rm xdebug.ini || echo "xdebug not available"
30 | - phpenv config-add .travis/ci.ini
31 |
32 | before_script:
33 | - cd $TRAVIS_BUILD_DIR
34 | - composer config -g process-timeout 900 && composer update
35 |
36 | script:
37 | - composer analyse
38 | - composer test
--------------------------------------------------------------------------------
/.travis/ci.ini:
--------------------------------------------------------------------------------
1 | [opcache]
2 | opcache.enable_cli=1
3 |
4 | [swoole]
5 | extension = "swoole.so"
6 |
--------------------------------------------------------------------------------
/.travis/swoole.install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | wget https://github.com/swoole/swoole-src/archive/v"${SW_VERSION}".tar.gz -O swoole.tar.gz
3 | mkdir -p swoole
4 | tar -xf swoole.tar.gz -C swoole --strip-components=1
5 | rm swoole.tar.gz
6 | cd swoole || exit
7 | phpize
8 | ./configure --enable-openssl --enable-mysqlnd --enable-http2
9 | make -j "$(nproc)"
10 | make install
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Sean Tymon
4 | Copyright (c) Eric Zhu
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hyperf JWT 组件
2 |
3 | 该组件基于 [`tymon/jwt-auth`](https://github.com/tymondesigns/jwt-auth ),实现了完整用于 JWT 认证的能力。
4 |
5 | 该组件并不直接提供身份认证的能力,你可以基于该组件提供的功能特性来实现自己的身份认证。
6 |
7 | 如果你不想自己动手,可以同时安装 [`hyperf-ext/auth`](https://github.com/hyperf-ext/auth) 组件来获得接近开箱即用的身份认证和授权功能。
8 |
9 | ## 安装
10 |
11 | ```shell script
12 | composer require hyperf-ext/jwt
13 | ```
14 |
15 | ## 发布配置
16 |
17 | ```shell script
18 | php bin/hyperf.php vendor:publish hyperf-ext/jwt
19 | ```
20 |
21 | > 文件位于 `config/autoload/jwt.php`。
22 |
23 | ## 配置
24 |
25 | ```php
26 | [
27 | /*
28 | |--------------------------------------------------------------------------
29 | | JWT 密钥
30 | |--------------------------------------------------------------------------
31 | |
32 | | 该密钥用于签名你的令牌,切记要在 .env 文件中设置。组件提供了一个辅助命令来完成
33 | | 这步操作:
34 | | `php bin/hyperf.php gen:jwt-secret`
35 | |
36 | | 注意:该密钥仅用于对称算法(HMAC),RSA 和 ECDSA 使用公私钥体系(见下方)。
37 | |
38 | | 注意:该值必须使用 BASE64 编码。
39 | |
40 | */
41 |
42 | 'secret' => env('JWT_SECRET'),
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | JWT 公私钥
47 | |--------------------------------------------------------------------------
48 | |
49 | | 你使用的算法将决定你的令牌是使用随机字符串(在 `JWT_SECRET` 中定设置)还是
50 | | 使用以下公钥和私钥来签名。组件提供了一个辅助命令来完成这步操作:
51 | | `php bin/hyperf.php gen:jwt-keypair`
52 | |
53 | | 对称算法:
54 | | HS256、HS384 和 HS512 使用 `JWT_SECRET`。
55 | |
56 | | 非对称算法:
57 | | RS256、RS384 和 RS512 / ES256、ES384 和 ES512 使用下面的公私钥。
58 | |
59 | */
60 |
61 | 'keys' => [
62 | /*
63 | |--------------------------------------------------------------------------
64 | | 公钥
65 | |--------------------------------------------------------------------------
66 | |
67 | | 你的公钥内容。
68 | |
69 | */
70 |
71 | 'public' => env('JWT_PUBLIC_KEY'),
72 |
73 | /*
74 | |--------------------------------------------------------------------------
75 | | 私钥
76 | |--------------------------------------------------------------------------
77 | |
78 | | 你的私钥内容。
79 | |
80 | */
81 |
82 | 'private' => env('JWT_PRIVATE_KEY'),
83 |
84 | /*
85 | |--------------------------------------------------------------------------
86 | | 密码
87 | |--------------------------------------------------------------------------
88 | |
89 | | 你的私钥的密码。不需要密码可设置为 `null`。
90 | |
91 | | 注意:该值必须使用 BASE64 编码。
92 | |
93 | */
94 |
95 | 'passphrase' => env('JWT_PASSPHRASE'),
96 | ],
97 |
98 | /*
99 | |--------------------------------------------------------------------------
100 | | JWT 生存时间
101 | |--------------------------------------------------------------------------
102 | |
103 | | 指定令牌有效的时长(以秒为单位)。默认为 1 小时。
104 | |
105 | | 你可以将其设置为 `null`,以产生永不过期的令牌。某些场景下有人可能想要这种行为,
106 | | 例如在用于手机应用的情况下。
107 | | 不太推荐这样做,因此请确保你有适当的体系来在必要时可以撤消令牌。
108 | | 注意:如果将其设置为 `null`,则应从 `required_claims` 列表中删除 `exp` 元素。
109 | |
110 | */
111 |
112 | 'ttl' => env('JWT_TTL', 3600),
113 |
114 | /*
115 | |--------------------------------------------------------------------------
116 | | 刷新生存时间
117 | |--------------------------------------------------------------------------
118 | |
119 | | 指定一个时长以在其有效期内可刷新令牌(以秒为单位)。 例如,用户可以
120 | | 在创建原始令牌后的 2 周内刷新该令牌,直到他们必须重新进行身份验证为止。
121 | | 默认为 2 周。
122 | |
123 | | 你可以将其设置为 `null`,以提供无限的刷新时间。某些场景下有人可能想要这种行为,
124 | | 而不是永不过期的令牌,例如在用于手机应用的情况下。
125 | | 不太推荐这样做,因此请确保你有适当的体系来在必要时可以撤消令牌。
126 | |
127 | */
128 |
129 | 'refresh_ttl' => env('JWT_REFRESH_TTL', 3600 * 24 * 14),
130 |
131 | /*
132 | |--------------------------------------------------------------------------
133 | | JWT 哈希算法
134 | |--------------------------------------------------------------------------
135 | |
136 | | 用于签名你的令牌的哈希算法。
137 | |
138 | | 关于算法的详细描述可参阅 https://tools.ietf.org/html/rfc7518。
139 | |
140 | | 可能的值:HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
141 | |
142 | */
143 |
144 | 'algo' => env('JWT_ALGO', 'HS512'),
145 |
146 | /*
147 | |--------------------------------------------------------------------------
148 | | 必要声明
149 | |--------------------------------------------------------------------------
150 | |
151 | | 指定在任一令牌中必须存在的必要声明。如果在有效载荷中不存在这些声明中的任意一个,
152 | | 则将抛出 `TokenInvalidException` 异常。
153 | |
154 | */
155 |
156 | 'required_claims' => [
157 | 'iss',
158 | 'iat',
159 | 'exp',
160 | 'nbf',
161 | 'sub',
162 | 'jti',
163 | ],
164 |
165 | /*
166 | |--------------------------------------------------------------------------
167 | | 保留声明
168 | |--------------------------------------------------------------------------
169 | |
170 | | 指定在刷新令牌时要保留的声明的键名。
171 | | 除了这些声明之外,`sub`、`iat` 和 `prv`(如果有)声明也将自动保留。
172 | |
173 | | 注意:如果有声明不存在,则会将其忽略。
174 | |
175 | */
176 |
177 | 'persistent_claims' => [
178 | // 'foo',
179 | // 'bar',
180 | ],
181 |
182 | /*
183 | |--------------------------------------------------------------------------
184 | | 锁定主题声明
185 | |--------------------------------------------------------------------------
186 | |
187 | | 这将决定是否将一个 `prv` 声明自动添加到令牌中。
188 | | 此目的是确保在你拥有多个身份验证模型时,例如 `App\User` 和 `App\OtherPerson`,
189 | | 如果两个令牌在两个不同的模型中碰巧具有相同的 ID(`sub` 声明),则我们应当防止
190 | | 一个身份验证请求冒充另一个身份验证请求。
191 | |
192 | | 在特定情况下,你可能需要禁用该行为,例如你只有一个身份验证模型的情况下,
193 | | 这可以减少一些令牌大小。
194 | |
195 | */
196 |
197 | 'lock_subject' => true,
198 |
199 | /*
200 | |--------------------------------------------------------------------------
201 | | 时间容差
202 | |--------------------------------------------------------------------------
203 | |
204 | | 该属性为 JWT 的时间戳类声明提供了一些时间上的容差。
205 | | 这意味着,如果你的某些服务器上不可避免地存在轻微的时钟偏差,
206 | | 那么这将可以为此提供一定程度的缓冲。
207 | |
208 | | 该设置适用于 `iat`、`nbf` 和 `exp`声明。
209 | | 以秒为单位设置该值,仅在你了解你真正需要它时才指定。
210 | |
211 | */
212 |
213 | 'leeway' => env('JWT_LEEWAY', 0),
214 |
215 | /*
216 | |--------------------------------------------------------------------------
217 | | 启用黑名单
218 | |--------------------------------------------------------------------------
219 | |
220 | | 为使令牌无效,你必须启用黑名单。
221 | | 如果你不想或不需要此功能,请将其设置为 `false`。
222 | |
223 | */
224 |
225 | 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
226 |
227 | /*
228 | | -------------------------------------------------------------------------
229 | | 黑名单宽限期
230 | | -------------------------------------------------------------------------
231 | |
232 | | 当使用同一个 JWT 发送多个并发请求时,由于每次请求都会重新生成令牌,
233 | | 因此其中一些可能会失败。
234 | |
235 | | 设置宽限期(以秒为单位)以防止并发请求失败。
236 | |
237 | */
238 |
239 | 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
240 |
241 | /*
242 | |--------------------------------------------------------------------------
243 | | 黑名单存储
244 | |--------------------------------------------------------------------------
245 | |
246 | | 指定用于实现在黑名单中存储令牌行为的类。
247 | |
248 | | 自定义存储类需要实现 `HyperfExt\Jwt\Contracts\StorageInterface` 接口。
249 | |
250 | */
251 |
252 | 'blacklist_storage' => HyperfExt\Jwt\Storage\HyperfCache::class,
253 | ];
254 | ```
255 |
256 | ## 使用
257 |
258 | 如果你使用 [`hyperf-ext/auth`](https://github.com/hyperf-ext/auth) 组件,则可以忽略该部分。
259 |
260 | ```php
261 | manager = $manager;
289 | $this->jwt = $jwtFactory->make();
290 | }
291 | }
292 | ```
293 |
294 | 可阅读上述两个类来详细了解如何使用。
295 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperf-ext/jwt",
3 | "type": "library",
4 | "license": "MIT",
5 | "keywords": [
6 | "php",
7 | "hyperf",
8 | "auth",
9 | "jwt"
10 | ],
11 | "description": "The Hyperf JWT package.",
12 | "authors": [
13 | {
14 | "name": "Eric Zhu",
15 | "email": "eric@zhu.email"
16 | },
17 | {
18 | "name": "Sean Tymon",
19 | "email": "tymon148@gmail.com",
20 | "homepage": "https://tymon.xyz",
21 | "role": "Developer"
22 | }
23 | ],
24 | "autoload": {
25 | "psr-4": {
26 | "HyperfExt\\Jwt\\": "src/"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "HyperfTest\\": "tests"
32 | }
33 | },
34 | "require": {
35 | "php": ">=7.3",
36 | "ext-swoole": ">=4.5",
37 | "ext-json": "*",
38 | "ext-openssl": "*",
39 | "hyperf/cache": "^2.1",
40 | "hyperf/command": "^2.1",
41 | "hyperf/config": "^2.1",
42 | "hyperf/di": "^2.1",
43 | "hyperf/framework": "^2.1",
44 | "lcobucci/jwt": "~4.1.0",
45 | "nesbot/carbon": "^2.0"
46 | },
47 | "require-dev": {
48 | "friendsofphp/php-cs-fixer": "^3.0",
49 | "hyperf/testing": "^2.1",
50 | "phpstan/phpstan": "^0.12",
51 | "swoole/ide-helper": "dev-master",
52 | "mockery/mockery": "^1.0"
53 | },
54 | "config": {
55 | "sort-packages": true
56 | },
57 | "scripts": {
58 | "test": "co-phpunit --prepend tests/bootstrap.php -c phpunit.xml --colors=always",
59 | "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src",
60 | "cs-fix": "php-cs-fixer fix $1"
61 | },
62 | "extra": {
63 | "hyperf": {
64 | "config": "HyperfExt\\Jwt\\ConfigProvider"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 | ./tests/
14 |
15 |
--------------------------------------------------------------------------------
/publish/jwt.php:
--------------------------------------------------------------------------------
1 | env('JWT_SECRET'),
29 |
30 | /*
31 | |--------------------------------------------------------------------------
32 | | JWT Authentication Keys
33 | |--------------------------------------------------------------------------
34 | |
35 | | The algorithm you are using, will determine whether your tokens are
36 | | signed with a random string (defined in `JWT_SECRET`) or using the
37 | | following public and private keys. A helper command is provided for this:
38 | | `php bin/hyperf.php gen:jwt-keypair`
39 | |
40 | | Symmetric Algorithms:
41 | | HS256, HS384 & HS512 will use `JWT_SECRET`.
42 | |
43 | | Asymmetric Algorithms:
44 | | RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
45 | |
46 | */
47 |
48 | 'keys' => [
49 | /*
50 | |--------------------------------------------------------------------------
51 | | Public Key
52 | |--------------------------------------------------------------------------
53 | |
54 | | Your public key content.
55 | |
56 | */
57 |
58 | 'public' => env('JWT_PUBLIC_KEY'),
59 |
60 | /*
61 | |--------------------------------------------------------------------------
62 | | Private Key
63 | |--------------------------------------------------------------------------
64 | |
65 | | Your private key content.
66 | |
67 | */
68 |
69 | 'private' => env('JWT_PRIVATE_KEY'),
70 |
71 | /*
72 | |--------------------------------------------------------------------------
73 | | Passphrase
74 | |--------------------------------------------------------------------------
75 | |
76 | | The passphrase for your private key. Can be null if none set.
77 | |
78 | | Note: This value must be encoded by base64.
79 | |
80 | */
81 |
82 | 'passphrase' => env('JWT_PASSPHRASE'),
83 | ],
84 |
85 | /*
86 | |--------------------------------------------------------------------------
87 | | JWT time to live
88 | |--------------------------------------------------------------------------
89 | |
90 | | Specify the length of time (in seconds) that the token will be valid for.
91 | | Defaults to 1 hour.
92 | |
93 | | You can also set this to null, to yield a never expiring token.
94 | | Some people may want this behaviour for e.g. a mobile app.
95 | | This is not particularly recommended, so make sure you have appropriate
96 | | systems in place to revoke the token if necessary.
97 | | Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
98 | |
99 | */
100 |
101 | 'ttl' => (int) env('JWT_TTL', 3600),
102 |
103 | /*
104 | |--------------------------------------------------------------------------
105 | | Refresh time to live
106 | |--------------------------------------------------------------------------
107 | |
108 | | Specify the length of time (in seconds) that the token can be refreshed
109 | | within. I.E. The user can refresh their token within a 2 week window of
110 | | the original token being created until they must re-authenticate.
111 | | Defaults to 2 weeks.
112 | |
113 | | You can also set this to null, to yield an infinite refresh time.
114 | | Some may want this instead of never expiring tokens for e.g. a mobile app.
115 | | This is not particularly recommended, so make sure you have appropriate
116 | | systems in place to revoke the token if necessary.
117 | |
118 | */
119 |
120 | 'refresh_ttl' => (int) env('JWT_REFRESH_TTL', 3600 * 24 * 14),
121 |
122 | /*
123 | |--------------------------------------------------------------------------
124 | | JWT hashing algorithm
125 | |--------------------------------------------------------------------------
126 | |
127 | | Specify the hashing algorithm that will be used to sign the token.
128 | |
129 | | possible values: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
130 | |
131 | */
132 |
133 | 'algo' => env('JWT_ALGO', 'HS512'),
134 |
135 | /*
136 | |--------------------------------------------------------------------------
137 | | Required Claims
138 | |--------------------------------------------------------------------------
139 | |
140 | | Specify the required claims that must exist in any token.
141 | | A TokenInvalidException will be thrown if any of these claims are not
142 | | present in the payload.
143 | |
144 | */
145 |
146 | 'required_claims' => [
147 | 'iss',
148 | 'iat',
149 | 'exp',
150 | 'nbf',
151 | 'sub',
152 | 'jti',
153 | ],
154 |
155 | /*
156 | |--------------------------------------------------------------------------
157 | | Persistent Claims
158 | |--------------------------------------------------------------------------
159 | |
160 | | Specify the claim keys to be persisted when refreshing a token.
161 | | `sub` and `iat` will automatically be persisted, in
162 | | addition to the these claims.
163 | |
164 | | Note: If a claim does not exist then it will be ignored.
165 | |
166 | */
167 |
168 | 'persistent_claims' => [
169 | // 'foo',
170 | // 'bar',
171 | ],
172 |
173 | /*
174 | |--------------------------------------------------------------------------
175 | | Lock Subject
176 | |--------------------------------------------------------------------------
177 | |
178 | | This will determine whether a `prv` claim is automatically added to
179 | | the token. The purpose of this is to ensure that if you have multiple
180 | | authentication models e.g. `App\User` & `App\OtherPerson`, then we
181 | | should prevent one authentication request from impersonating another,
182 | | if 2 tokens happen to have the same id across the 2 different models.
183 | |
184 | | Under specific circumstances, you may want to disable this behaviour
185 | | e.g. if you only have one authentication model, then you would save
186 | | a little on token size.
187 | |
188 | */
189 |
190 | 'lock_subject' => true,
191 |
192 | /*
193 | |--------------------------------------------------------------------------
194 | | Leeway
195 | |--------------------------------------------------------------------------
196 | |
197 | | This property gives the jwt timestamp claims some "leeway".
198 | | Meaning that if you have any unavoidable slight clock skew on
199 | | any of your servers then this will afford you some level of cushioning.
200 | |
201 | | This applies to the claims `iat`, `nbf` and `exp`.
202 | |
203 | | Specify in seconds - only if you know you need it.
204 | |
205 | */
206 |
207 | 'leeway' => (int) env('JWT_LEEWAY', 0),
208 |
209 | /*
210 | |--------------------------------------------------------------------------
211 | | Blacklist Enabled
212 | |--------------------------------------------------------------------------
213 | |
214 | | In order to invalidate tokens, you must have the blacklist enabled.
215 | | If you do not want or need this functionality, then set this to false.
216 | |
217 | */
218 |
219 | 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
220 |
221 | /*
222 | | -------------------------------------------------------------------------
223 | | Blacklist Grace Period
224 | | -------------------------------------------------------------------------
225 | |
226 | | When multiple concurrent requests are made with the same JWT,
227 | | it is possible that some of them fail, due to token regeneration
228 | | on every request.
229 | |
230 | | Set grace period in seconds to prevent parallel request failure.
231 | |
232 | */
233 |
234 | 'blacklist_grace_period' => (int) env('JWT_BLACKLIST_GRACE_PERIOD', 0),
235 |
236 | /*
237 | |--------------------------------------------------------------------------
238 | | Blacklist Storage
239 | |--------------------------------------------------------------------------
240 | |
241 | | Specify the handler that is used to store tokens in the blacklist.
242 | |
243 | */
244 |
245 | 'blacklist_storage' => HyperfExt\Jwt\Storage\HyperfCache::class,
246 | ];
247 |
--------------------------------------------------------------------------------
/src/Blacklist.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
51 | $this->gracePeriod = $gracePeriod;
52 | $this->refreshTtl = $refreshTtl;
53 | }
54 |
55 | /**
56 | * Add the token (jti claim) to the blacklist.
57 | */
58 | public function add(Payload $payload): bool
59 | {
60 | // if there is no exp claim then add the jwt to
61 | // the blacklist indefinitely
62 | if (! $payload->hasKey('exp')) {
63 | return $this->addForever($payload);
64 | }
65 |
66 | // if we have already added this token to the blacklist
67 | if (! empty($this->storage->get($this->getKey($payload)))) {
68 | return true;
69 | }
70 |
71 | $this->storage->add(
72 | $this->getKey($payload),
73 | ['valid_until' => $this->getGraceTimestamp()],
74 | $this->getSecondsUntilExpired($payload)
75 | );
76 |
77 | return true;
78 | }
79 |
80 | /**
81 | * Add the token (jti claim) to the blacklist indefinitely.
82 | */
83 | public function addForever(Payload $payload): bool
84 | {
85 | $this->storage->forever($this->getKey($payload), 'forever');
86 |
87 | return true;
88 | }
89 |
90 | /**
91 | * Determine whether the token has been blacklisted.
92 | */
93 | public function has(Payload $payload): bool
94 | {
95 | $val = $this->storage->get((string) $this->getKey($payload));
96 |
97 | // exit early if the token was blacklisted forever,
98 | if ($val === 'forever') {
99 | return true;
100 | }
101 |
102 | // check whether the expiry + grace has past
103 | return ! empty($val) and ! Utils::isFuture($val['valid_until']);
104 | }
105 |
106 | /**
107 | * Remove the token (jti claim) from the blacklist.
108 | */
109 | public function remove(Payload $payload): bool
110 | {
111 | return $this->storage->destroy($this->getKey($payload));
112 | }
113 |
114 | /**
115 | * Remove all tokens from the blacklist.
116 | */
117 | public function clear(): bool
118 | {
119 | $this->storage->flush();
120 |
121 | return true;
122 | }
123 |
124 | /**
125 | * Set the grace period.
126 | *
127 | * @return $this
128 | */
129 | public function setGracePeriod(int $gracePeriod)
130 | {
131 | $this->gracePeriod = (int) $gracePeriod;
132 |
133 | return $this;
134 | }
135 |
136 | /**
137 | * Get the grace period.
138 | */
139 | public function getGracePeriod(): int
140 | {
141 | return $this->gracePeriod;
142 | }
143 |
144 | /**
145 | * Get the unique key held within the blacklist.
146 | *
147 | * @return mixed
148 | */
149 | public function getKey(Payload $payload)
150 | {
151 | return $payload($this->key);
152 | }
153 |
154 | /**
155 | * Set the unique key held within the blacklist.
156 | *
157 | * @return $this
158 | */
159 | public function setKey(string $key)
160 | {
161 | $this->key = value($key);
162 |
163 | return $this;
164 | }
165 |
166 | /**
167 | * Set the refresh time limit.
168 | *
169 | * @return $this
170 | */
171 | public function setRefreshTtl(?int $refreshTtl)
172 | {
173 | $this->refreshTtl = $refreshTtl === null ? null : (int) $refreshTtl;
174 |
175 | return $this;
176 | }
177 |
178 | /**
179 | * Get the refresh time limit.
180 | */
181 | public function getRefreshTtl(): ?int
182 | {
183 | return $this->refreshTtl;
184 | }
185 |
186 | /**
187 | * Get the number of seconds until the token expiry.
188 | */
189 | protected function getSecondsUntilExpired(Payload $payload): int
190 | {
191 | $exp = Utils::timestamp($payload['exp']);
192 | $iat = Utils::timestamp($payload['iat']);
193 |
194 | // get the latter of the two expiration dates and find
195 | // the number of seconds until the expiration date,
196 | // plus 1 minute to avoid overlap
197 | return $exp->max($iat->addSeconds($this->refreshTtl))->addMinute()->diffInRealSeconds();
198 | }
199 |
200 | /**
201 | * Get the timestamp when the blacklist comes into effect
202 | * This defaults to immediate (0 seconds).
203 | */
204 | protected function getGraceTimestamp(): int
205 | {
206 | return Utils::now()->addSeconds($this->gracePeriod)->getTimestamp();
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/Claims/AbstractClaim.php:
--------------------------------------------------------------------------------
1 | setValue($value);
47 | }
48 |
49 | /**
50 | * Get the payload as a string.
51 | */
52 | public function __toString(): string
53 | {
54 | return $this->toJson();
55 | }
56 |
57 | /**
58 | * Set the claim value, and call a validate method.
59 | *
60 | * @param mixed $value
61 | *
62 | * @return $this
63 | */
64 | public function setValue($value)
65 | {
66 | $this->value = $this->validateCreate($value);
67 |
68 | return $this;
69 | }
70 |
71 | /**
72 | * Get the claim value.
73 | *
74 | * @return mixed
75 | */
76 | public function getValue()
77 | {
78 | return $this->value;
79 | }
80 |
81 | /**
82 | * Set the claim name.
83 | *
84 | * @return $this
85 | */
86 | public function setName(string $name)
87 | {
88 | $this->name = $name;
89 |
90 | return $this;
91 | }
92 |
93 | /**
94 | * Get the claim name.
95 | */
96 | public function getName(): string
97 | {
98 | return $this->name;
99 | }
100 |
101 | /**
102 | * Validate the claim in a standalone Claim context.
103 | *
104 | * @param mixed $value
105 | */
106 | public function validateCreate($value)
107 | {
108 | return $value;
109 | }
110 |
111 | /**
112 | * Checks if the value matches the claim.
113 | *
114 | * @param mixed $value
115 | */
116 | public function matches($value, bool $strict = true): bool
117 | {
118 | return $strict ? $this->value === $value : $this->value == $value;
119 | }
120 |
121 | /**
122 | * Convert the object into something JSON serializable.
123 | */
124 | public function jsonSerialize(): array
125 | {
126 | return $this->toArray();
127 | }
128 |
129 | /**
130 | * Build a key value array comprising of the claim name and value.
131 | */
132 | public function toArray(): array
133 | {
134 | return [$this->getName() => $this->getValue()];
135 | }
136 |
137 | /**
138 | * Get the claim as JSON.
139 | */
140 | public function toJson(int $options = JSON_UNESCAPED_SLASHES): string
141 | {
142 | return json_encode($this->toArray(), $options);
143 | }
144 |
145 | protected function getFactory(): Factory
146 | {
147 | if (! empty($this->factory)) {
148 | return $this->factory;
149 | }
150 | return $this->factory = ApplicationContext::getContainer()->get(ManagerInterface::class)->getClaimFactory();
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Claims/Audience.php:
--------------------------------------------------------------------------------
1 | getArrayableItems($items));
25 | }
26 |
27 | /**
28 | * Get a Claim instance by it's unique name.
29 | *
30 | * @param mixed $default
31 | */
32 | public function getByClaimName(string $name, ?callable $callback = null, $default = null): AbstractClaim
33 | {
34 | return $this->filter(function (AbstractClaim $claim) use ($name) {
35 | return $claim->getName() === $name;
36 | })->first($callback, $default);
37 | }
38 |
39 | /**
40 | * Validate each claim.
41 | *
42 | * @return $this
43 | */
44 | public function validate(bool $ignoreExpired = false)
45 | {
46 | $this->each(function ($claim) use ($ignoreExpired) {
47 | $claim->validate($ignoreExpired);
48 | });
49 | return $this;
50 | }
51 |
52 | /**
53 | * Determine if the Collection contains all of the given keys.
54 | *
55 | * @param mixed $claims
56 | */
57 | public function hasAllClaims($claims): bool
58 | {
59 | return count($claims) and (new static($claims))->diff($this->keys())->isEmpty();
60 | }
61 |
62 | /**
63 | * Get the claims as key/val array.
64 | */
65 | public function toPlainArray(): array
66 | {
67 | return $this->map(function (AbstractClaim $claim) {
68 | return $claim->getValue();
69 | })->toArray();
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | protected function getArrayableItems($items): array
76 | {
77 | return $this->sanitizeClaims($items);
78 | }
79 |
80 | /**
81 | * Ensure that the given claims array is keyed by the claim name.
82 | *
83 | * @param mixed $items
84 | */
85 | private function sanitizeClaims($items): array
86 | {
87 | $claims = [];
88 | foreach ($items as $key => $value) {
89 | if (! is_string($key) and $value instanceof AbstractClaim) {
90 | $key = $value->getName();
91 | }
92 |
93 | $claims[$key] = $value;
94 | }
95 |
96 | return $claims;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Claims/Custom.php:
--------------------------------------------------------------------------------
1 | setName($name);
22 | }
23 |
24 | public function validate(bool $ignoreExpired = false): bool
25 | {
26 | return true;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Claims/DatetimeTrait.php:
--------------------------------------------------------------------------------
1 | add($value);
40 | }
41 |
42 | if ($value instanceof DateTimeInterface) {
43 | $value = $value->getTimestamp();
44 | }
45 |
46 | return parent::setValue($value);
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function validateCreate($value)
53 | {
54 | if (! is_numeric($value)) {
55 | throw new InvalidClaimException($this);
56 | }
57 |
58 | return $value;
59 | }
60 |
61 | /**
62 | * Set the leeway in seconds.
63 | *
64 | * @return $this
65 | */
66 | public function setLeeway(int $leeway)
67 | {
68 | $this->leeway = $leeway;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * Determine whether the value is in the future.
75 | *
76 | * @param mixed $value
77 | */
78 | protected function isFuture($value): bool
79 | {
80 | return Utils::isFuture((int) $value, (int) $this->leeway);
81 | }
82 |
83 | /**
84 | * Determine whether the value is in the past.
85 | *
86 | * @param mixed $value
87 | */
88 | protected function isPast($value): bool
89 | {
90 | return Utils::isPast((int) $value, (int) $this->leeway);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Claims/Expiration.php:
--------------------------------------------------------------------------------
1 | isPast($this->getValue())) {
24 | throw new TokenExpiredException('Token has expired');
25 | }
26 | return true;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Claims/Factory.php:
--------------------------------------------------------------------------------
1 | Audience::class,
48 | 'exp' => Expiration::class,
49 | 'iat' => IssuedAt::class,
50 | 'iss' => Issuer::class,
51 | 'jti' => JwtId::class,
52 | 'nbf' => NotBefore::class,
53 | 'sub' => Subject::class,
54 | ];
55 |
56 | public function __construct(?int $ttl, ?int $refreshTtl, int $leeway = 0)
57 | {
58 | $this->setTtl($ttl);
59 | $this->setRefreshTtl($refreshTtl);
60 | $this->setLeeway($leeway);
61 | }
62 |
63 | /**
64 | * Get the instance of the claim when passing the name and value.
65 | *
66 | * @param mixed $value
67 | */
68 | public function get(string $name, $value): ClaimInterface
69 | {
70 | if ($this->has($name)) {
71 | $claim = make($this->classMap[$name], ['factory' => $this, 'value' => $value]);
72 |
73 | return method_exists($claim, 'setLeeway') ?
74 | $claim->setLeeway($this->leeway) :
75 | $claim;
76 | }
77 |
78 | return new Custom($name, $value);
79 | }
80 |
81 | /**
82 | * Check whether the claim exists.
83 | */
84 | public function has(string $name): bool
85 | {
86 | return array_key_exists($name, $this->classMap);
87 | }
88 |
89 | /**
90 | * Generate the initial value and return the Claim instance.
91 | */
92 | public function make(string $name): ClaimInterface
93 | {
94 | return $this->get($name, $this->{$name}());
95 | }
96 |
97 | /**
98 | * Add a new claim mapping.
99 | *
100 | * @return $this
101 | */
102 | public function extend(string $name, string $classPath)
103 | {
104 | $this->classMap[$name] = $classPath;
105 |
106 | return $this;
107 | }
108 |
109 | /**
110 | * Set the token ttl (in seconds).
111 | *
112 | * @return $this
113 | */
114 | public function setTtl(?int $ttl)
115 | {
116 | $this->ttl = $ttl === null ? null : (int) $ttl;
117 |
118 | return $this;
119 | }
120 |
121 | /**
122 | * Get the token ttl.
123 | */
124 | public function getTtl(): ?int
125 | {
126 | return $this->ttl;
127 | }
128 |
129 | /**
130 | * Set the token refresh ttl (in seconds).
131 | *
132 | * @return $this
133 | */
134 | public function setRefreshTtl(?int $refreshTtl)
135 | {
136 | $this->refreshTtl = $refreshTtl === null ? null : (int) $refreshTtl;
137 |
138 | return $this;
139 | }
140 |
141 | /**
142 | * Get the token refresh ttl.
143 | */
144 | public function getRefreshTtl(): ?int
145 | {
146 | return $this->refreshTtl;
147 | }
148 |
149 | /**
150 | * Set the leeway in seconds.
151 | *
152 | * @return $this
153 | */
154 | public function setLeeway(int $leeway)
155 | {
156 | $this->leeway = $leeway;
157 |
158 | return $this;
159 | }
160 |
161 | public function iss(): string
162 | {
163 | return ApplicationContext::getContainer()->get(ServerRequestInterface::class)->url();
164 | }
165 |
166 | public function iat(): int
167 | {
168 | return time();
169 | }
170 |
171 | public function exp(): int
172 | {
173 | return time() + $this->getTtl();
174 | }
175 |
176 | public function nbf(): int
177 | {
178 | return time();
179 | }
180 |
181 | public function jti(): string
182 | {
183 | return Str::random(16);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/Claims/IssuedAt.php:
--------------------------------------------------------------------------------
1 | commonValidateCreate($value);
28 |
29 | if ($this->isFuture($value)) {
30 | throw new InvalidClaimException($this);
31 | }
32 |
33 | return $value;
34 | }
35 |
36 | public function validate(bool $ignoreExpired = false): bool
37 | {
38 | if ($this->isFuture($value = $this->getValue())) {
39 | throw new TokenInvalidException('Issued At (iat) timestamp cannot be in the future');
40 | }
41 |
42 | if (
43 | ($refreshTtl = $this->getFactory()->getRefreshTtl()) !== null && $this->isPast($value + $refreshTtl)
44 | ) {
45 | throw new TokenExpiredException('Token has expired and can no longer be refreshed');
46 | }
47 |
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Claims/Issuer.php:
--------------------------------------------------------------------------------
1 | isFuture($this->getValue())) {
24 | throw new TokenInvalidException('Not Before (nbf) timestamp cannot be in the future');
25 | }
26 | return true;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Claims/Subject.php:
--------------------------------------------------------------------------------
1 | HS256::class,
45 | 'HS384' => HS384::class,
46 | 'HS512' => HS512::class,
47 | 'RS256' => RS256::class,
48 | 'RS384' => RS384::class,
49 | 'RS512' => RS512::class,
50 | 'ES256' => ES256::class,
51 | 'ES384' => ES384::class,
52 | 'ES512' => ES512::class,
53 | ];
54 |
55 | protected $asymmetric = [
56 | 'HS256' => false,
57 | 'HS384' => false,
58 | 'HS512' => false,
59 | 'RS256' => true,
60 | 'RS384' => true,
61 | 'RS512' => true,
62 | 'ES256' => true,
63 | 'ES384' => true,
64 | 'ES512' => true,
65 | ];
66 |
67 | /**
68 | * The secret.
69 | *
70 | * @var string
71 | */
72 | protected $secret;
73 |
74 | /**
75 | * The array of keys.
76 | *
77 | * @var array
78 | */
79 | protected $keys;
80 |
81 | /**
82 | * The used algorithm.
83 | *
84 | * @var string
85 | */
86 | protected $algo;
87 |
88 | /**
89 | * The Configuration instance.
90 | *
91 | * @var \Lcobucci\JWT\Configuration
92 | */
93 | protected $config;
94 |
95 | /**
96 | * The Signer instance.
97 | *
98 | * @var \Lcobucci\JWT\Signer
99 | */
100 | protected $signer;
101 |
102 | /**
103 | * @param null|\Lcobucci\JWT\Configuration $config
104 | *
105 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
106 | */
107 | public function __construct(string $secret, string $algo, array $keys, $config = null)
108 | {
109 | $this->secret = $secret;
110 | $this->algo = $algo;
111 | $this->keys = $keys;
112 | $this->config = $config;
113 |
114 | $this->signer = $this->getSigner();
115 |
116 | if (! is_null($config)) {
117 | $this->config = $config;
118 | } elseif ($this->isAsymmetric()) {
119 | $this->config = Configuration::forAsymmetricSigner($this->signer, $this->getSigningKey(), $this->getVerificationKey());
120 | } else {
121 | $this->config = Configuration::forSymmetricSigner($this->signer, InMemory::plainText($this->getSecret()));
122 | }
123 | if (! count($this->config->validationConstraints())) {
124 | $this->config->setValidationConstraints(
125 | new SignedWith($this->signer, $this->getVerificationKey()),
126 | );
127 | }
128 | }
129 |
130 | /**
131 | * Set the algorithm used to sign the token.
132 | *
133 | * @return $this
134 | */
135 | public function setAlgo(string $algo)
136 | {
137 | $this->algo = $algo;
138 |
139 | return $this;
140 | }
141 |
142 | /**
143 | * Get the algorithm used to sign the token.
144 | */
145 | public function getAlgo(): string
146 | {
147 | return $this->algo;
148 | }
149 |
150 | /**
151 | * Set the secret used to sign the token.
152 | *
153 | * @return $this
154 | */
155 | public function setSecret(string $secret)
156 | {
157 | $this->secret = $secret;
158 |
159 | return $this;
160 | }
161 |
162 | /**
163 | * Get the secret used to sign the token.
164 | *
165 | * @return string
166 | */
167 | public function getSecret()
168 | {
169 | return $this->secret;
170 | }
171 |
172 | /**
173 | * Set the keys used to sign the token.
174 | *
175 | * @return $this
176 | */
177 | public function setKeys(array $keys)
178 | {
179 | $this->keys = $keys;
180 |
181 | return $this;
182 | }
183 |
184 | /**
185 | * Get the array of keys used to sign tokens
186 | * with an asymmetric algorithm.
187 | */
188 | public function getKeys(): array
189 | {
190 | return $this->keys;
191 | }
192 |
193 | /**
194 | * Get the public key used to sign tokens
195 | * with an asymmetric algorithm.
196 | *
197 | * @return resource|string
198 | */
199 | public function getPublicKey()
200 | {
201 | return Arr::get($this->keys, 'public');
202 | }
203 |
204 | /**
205 | * Get the private key used to sign tokens
206 | * with an asymmetric algorithm.
207 | *
208 | * @return resource|string
209 | */
210 | public function getPrivateKey()
211 | {
212 | return Arr::get($this->keys, 'private');
213 | }
214 |
215 | /**
216 | * Get the passphrase used to sign tokens
217 | * with an asymmetric algorithm.
218 | */
219 | public function getPassphrase(): ?string
220 | {
221 | return Arr::get($this->keys, 'passphrase');
222 | }
223 |
224 | /**
225 | * Create a JSON Web Token.
226 | *
227 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
228 | */
229 | public function encode(array $payload): string
230 | {
231 | $builder = $this->getBuilder();
232 |
233 | try {
234 | foreach ($payload as $key => $value) {
235 | $this->addClaim($builder, $key, $value);
236 | }
237 | return $builder->getToken($this->config->signer(), $this->config->signingKey())->toString();
238 | } catch (Exception $e) {
239 | throw new JwtException('Could not create token: ' . $e->getMessage(), $e->getCode(), $e);
240 | }
241 | }
242 |
243 | /**
244 | * Decode a JSON Web Token.
245 | *
246 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
247 | */
248 | public function decode(string $token): array
249 | {
250 | $parser = $this->getParser();
251 |
252 | try {
253 | $jwt = $parser->parse($token);
254 | } catch (Exception $e) {
255 | throw new TokenInvalidException('Could not decode token: ' . $e->getMessage(), $e->getCode(), $e);
256 | }
257 |
258 | if (! $this->config->validator()->validate($jwt, ...$this->config->validationConstraints())) {
259 | throw new TokenInvalidException('Token Signature could not be verified.');
260 | }
261 | return (new Collection($jwt->claims()->all()))->map(function ($claim) {
262 | if (is_a($claim, \DateTimeImmutable::class)) {
263 | return $claim->getTimestamp();
264 | }
265 | if (is_object($claim) && method_exists($claim, 'getValue')) {
266 | return $claim->getValue();
267 | }
268 |
269 | return $claim;
270 | })->toArray();
271 | }
272 |
273 | /**
274 | * Gets the {@see $config} attribute.
275 | *
276 | * @return \Lcobucci\JWT\Configuration
277 | */
278 | public function getConfig()
279 | {
280 | return $this->config;
281 | }
282 |
283 | /**
284 | * Adds a claim to the {@see $config}.
285 | *
286 | * @param mixed $value
287 | */
288 | protected function addClaim(Builder $builder, string $key, $value)
289 | {
290 | switch ($key) {
291 | case RegisteredClaims::ID:
292 | $builder->identifiedBy((string) $value);
293 | break;
294 | case RegisteredClaims::EXPIRATION_TIME:
295 | $builder->expiresAt(\DateTimeImmutable::createFromFormat('U', (string) $value));
296 | break;
297 | case RegisteredClaims::NOT_BEFORE:
298 | $builder->canOnlyBeUsedAfter(\DateTimeImmutable::createFromFormat('U', (string) $value));
299 | break;
300 | case RegisteredClaims::ISSUED_AT:
301 | $builder->issuedAt(\DateTimeImmutable::createFromFormat('U', (string) $value));
302 | break;
303 | case RegisteredClaims::ISSUER:
304 | $builder->issuedBy((string) $value);
305 | break;
306 | case RegisteredClaims::AUDIENCE:
307 | $builder->permittedFor((string) $value);
308 | break;
309 | case RegisteredClaims::SUBJECT:
310 | $builder->relatedTo((string) $value);
311 | break;
312 | default:
313 | $builder->withClaim($key, $value);
314 | }
315 | }
316 |
317 | /**
318 | * Get the signer instance.
319 | *
320 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
321 | */
322 | protected function getSigner(): Signer
323 | {
324 | if ($this->signer !== null) {
325 | return $this->signer;
326 | }
327 |
328 | if (! array_key_exists($this->algo, $this->signers)) {
329 | throw new JwtException('The given algorithm could not be found');
330 | }
331 |
332 | return $this->signer = new $this->signers[$this->algo]();
333 | }
334 |
335 | /**
336 | * Get the builder instance.
337 | */
338 | protected function getBuilder(): Builder
339 | {
340 | return $this->config->builder();
341 | }
342 |
343 | /**
344 | * Get the parser instance.
345 | */
346 | protected function getParser(): Parser
347 | {
348 | return $this->config->parser();
349 | }
350 |
351 | /**
352 | * Determine if the algorithm is asymmetric, and thus
353 | * requires a public/private key combo.
354 | */
355 | protected function isAsymmetric(): bool
356 | {
357 | return $this->asymmetric[$this->algo];
358 | }
359 |
360 | /**
361 | * Get the key used to sign the tokens.
362 | */
363 | protected function getSigningKey(): Signer\Key
364 | {
365 | return $this->isAsymmetric()
366 | ? InMemory::plainText($this->getPrivateKey(), $this->getPassphrase() ?? '')
367 | : InMemory::plainText($this->getSecret());
368 | }
369 |
370 | /**
371 | * Get the key used to verify the tokens.
372 | */
373 | protected function getVerificationKey(): Signer\Key
374 | {
375 | return $this->isAsymmetric()
376 | ? InMemory::plainText($this->getPublicKey())
377 | : InMemory::plainText($this->getSecret());
378 | }
379 | }
380 |
--------------------------------------------------------------------------------
/src/Commands/AbstractGenCommand.php:
--------------------------------------------------------------------------------
1 | config = $config;
30 | }
31 |
32 | public function configure()
33 | {
34 | parent::configure();
35 | $this->setDescription($this->description);
36 | $this->addOption('show', 's', InputOption::VALUE_NONE, 'Display the key instead of modifying files');
37 | $this->addOption('always-no', null, InputOption::VALUE_NONE, 'Skip generating key if it already exists');
38 | $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip confirmation when overwriting an existing key');
39 | }
40 |
41 | /**
42 | * @param null|mixed $default
43 | *
44 | * @return null|mixed
45 | */
46 | protected function getOption(string $name, $default = null)
47 | {
48 | $result = $this->input->getOption($name);
49 | return empty($result) ? $default : $result;
50 | }
51 |
52 | protected function envFilePath(): string
53 | {
54 | return BASE_PATH . '/.env';
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Commands/GenJwtKeypairCommand.php:
--------------------------------------------------------------------------------
1 | ['private_key_type' => OPENSSL_KEYTYPE_RSA, 'digest_alg' => 'SHA256', 'private_key_bits' => 4096],
23 | 'RS384' => ['private_key_type' => OPENSSL_KEYTYPE_RSA, 'digest_alg' => 'SHA384', 'private_key_bits' => 4096],
24 | 'RS512' => ['private_key_type' => OPENSSL_KEYTYPE_RSA, 'digest_alg' => 'SHA512', 'private_key_bits' => 4096],
25 | 'ES256' => ['private_key_type' => OPENSSL_KEYTYPE_EC, 'digest_alg' => 'SHA256', 'curve_name' => 'secp256k1'],
26 | 'ES384' => ['private_key_type' => OPENSSL_KEYTYPE_EC, 'digest_alg' => 'SHA384', 'curve_name' => 'secp384r1'],
27 | 'ES512' => ['private_key_type' => OPENSSL_KEYTYPE_EC, 'digest_alg' => 'SHA512', 'curve_name' => 'secp521r1'],
28 | ];
29 |
30 | public function handle()
31 | {
32 | [, $config] = $this->choiceAlgorithm();
33 | $passphrase = $this->setPassphrase();
34 |
35 | [$privateKey, $publicKey] = $this->generateKeypair($config, $passphrase);
36 |
37 | if (! empty($passphrase)) {
38 | $passphrase = base64_encode($passphrase);
39 | }
40 |
41 | if ($this->getOption('show')) {
42 | $this->displayKey($privateKey, $publicKey, $passphrase);
43 | return;
44 | }
45 |
46 | if (file_exists($path = $this->envFilePath()) === false) {
47 | $this->displayKey($privateKey, $publicKey, $passphrase);
48 | return;
49 | }
50 |
51 | if (Str::contains(file_get_contents($path), ['JWT_PRIVATE_KEY', 'JWT_PUBLIC_KEY', 'JWT_PASSPHRASE'])) {
52 | if ($this->getOption('always-no')) {
53 | $this->comment('The key pair or some part of it already exists. Skipping...');
54 | return;
55 | }
56 |
57 | if ($this->isConfirmed() === false) {
58 | $this->comment('Phew... No changes were made to your key pair.');
59 | return;
60 | }
61 |
62 | $force = true;
63 | } else {
64 | $force = false;
65 | }
66 |
67 | foreach (['privateKey', 'publicKey', 'passphrase'] as $name) {
68 | $this->writeEnv($path, $name, ${$name}, $force);
69 | }
70 |
71 | $this->info('JWT key pair set successfully.');
72 | }
73 |
74 | protected function writeEnv(string $path, string $name, ?string $value, bool $force)
75 | {
76 | $envKey = 'JWT_' . Str::upper(Str::snake($name));
77 | $envValue = empty($value) ? '(null)' : '"' . str_replace("\n", '\\n', $value) . '"';
78 |
79 | if (Str::contains(file_get_contents($path), $envKey) === false) {
80 | file_put_contents($path, "{$envKey}={$envValue}\n", FILE_APPEND);
81 | } elseif ($force) {
82 | file_put_contents($path, preg_replace(
83 | "~{$envKey}=[^\n]*~",
84 | "{$envKey}={$envValue}",
85 | file_get_contents($path)
86 | ));
87 | }
88 | }
89 |
90 | protected function choiceAlgorithm(): array
91 | {
92 | $algo = $this->choice('Select algorithm', array_keys($this->configs));
93 | return [$algo, $this->configs[$algo]];
94 | }
95 |
96 | protected function setPassphrase(): ?string
97 | {
98 | $random = $this->choice('Use random passphrase', ['Yes', 'No']);
99 | if ($random === 'Yes') {
100 | return random_bytes(16);
101 | }
102 | return $this->ask('Set passphrase (can be empty)');
103 | }
104 |
105 | protected function generateKeypair(array $config, ?string $passphrase = null): array
106 | {
107 | $res = openssl_pkey_new($config);
108 | openssl_pkey_export($res, $privateKey, $passphrase);
109 | $publicKey = openssl_pkey_get_details($res)['key'];
110 | return [$privateKey, $publicKey];
111 | }
112 |
113 | protected function isConfirmed(): bool
114 | {
115 | return $this->getOption('force') ? true : $this->confirm(
116 | 'Are you sure you want to override the key pair? This will invalidate all existing tokens.'
117 | );
118 | }
119 |
120 | protected function displayKey(string $privateKey, string $publicKey, ?string $passphrase): void
121 | {
122 | $this->info('Private Key:');
123 | $this->comment($privateKey);
124 | $this->info('Public Key:');
125 | $this->comment($publicKey);
126 | $this->info('Passphrase (base64 encoded):');
127 | $this->comment(empty($passphrase) ? 'No Passphrase' : $passphrase);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Commands/GenJwtSecretCommand.php:
--------------------------------------------------------------------------------
1 | getOption('show')) {
26 | $this->comment($key);
27 | return;
28 | }
29 |
30 | if (file_exists($path = $this->envFilePath()) === false) {
31 | $this->displayKey($key);
32 | return;
33 | }
34 |
35 | if (Str::contains(file_get_contents($path), 'JWT_SECRET') === false) {
36 | file_put_contents($path, "\nJWT_SECRET={$key}\n", FILE_APPEND);
37 | } else {
38 | if ($this->getOption('always-no')) {
39 | $this->comment('Secret key already exists. Skipping...');
40 | return;
41 | }
42 |
43 | if ($this->isConfirmed() === false) {
44 | $this->comment('Phew... No changes were made to your secret key.');
45 | return;
46 | }
47 |
48 | file_put_contents($path, preg_replace(
49 | "~JWT_SECRET=[^\n]*~",
50 | "JWT_SECRET=\"{$key}\"",
51 | file_get_contents($path)
52 | ));
53 | }
54 |
55 | $this->displayKey($key);
56 | }
57 |
58 | protected function displayKey(string $key): void
59 | {
60 | $this->info("JWT secret [{$key}] (base64 encoded) set successfully.");
61 | }
62 |
63 | protected function isConfirmed(): bool
64 | {
65 | return $this->getOption('force') ? true : $this->confirm(
66 | 'Are you sure you want to override the key? This will invalidate all existing tokens.'
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/ConfigProvider.php:
--------------------------------------------------------------------------------
1 | [
30 | ManagerInterface::class => ManagerFactory::class,
31 | TokenValidatorInterface::class => TokenValidator::class,
32 | PayloadValidatorInterface::class => PayloadValidator::class,
33 | RequestParserInterface::class => RequestParserFactory::class,
34 | JwtFactoryInterface::class => JwtFactory::class,
35 | ],
36 | 'commands' => [
37 | GenJwtSecretCommand::class,
38 | GenJwtKeypairCommand::class,
39 | ],
40 | 'publish' => [
41 | [
42 | 'id' => 'config',
43 | 'description' => 'The config for hyperf-ext/jwt.',
44 | 'source' => __DIR__ . '/../publish/jwt.php',
45 | 'destination' => BASE_PATH . '/config/autoload/jwt.php',
46 | ],
47 | ],
48 | ];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Contracts/ClaimInterface.php:
--------------------------------------------------------------------------------
1 | customClaims = $customClaims;
30 |
31 | return $this;
32 | }
33 |
34 | /**
35 | * Get the custom claims.
36 | */
37 | public function getCustomClaims(): array
38 | {
39 | return $this->customClaims;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidClaimException.php:
--------------------------------------------------------------------------------
1 | getName() . ']', $code, $previous);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidConfigException.php:
--------------------------------------------------------------------------------
1 | manager = $manager;
53 | $this->requestParser = $requestParser;
54 | $this->request = $request;
55 | }
56 |
57 | /**
58 | * Magically call the Jwt Manager.
59 | *
60 | * @throws \BadMethodCallException
61 | *
62 | * @return mixed
63 | */
64 | public function __call(string $method, array $parameters)
65 | {
66 | if (method_exists($this->manager, $method)) {
67 | return call_user_func_array([$this->manager, $method], $parameters);
68 | }
69 |
70 | throw new BadMethodCallException("Method [{$method}] does not exist.");
71 | }
72 |
73 | /**
74 | * Generate a token for a given subject.
75 | */
76 | public function fromSubject(JwtSubjectInterface $subject): string
77 | {
78 | $payload = $this->makePayload($subject);
79 |
80 | return $this->manager->encode($payload)->get();
81 | }
82 |
83 | /**
84 | * Alias to generate a token for a given user.
85 | */
86 | public function fromUser(JwtSubjectInterface $user): string
87 | {
88 | return $this->fromSubject($user);
89 | }
90 |
91 | /**
92 | * Refresh an expired token.
93 | *
94 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
95 | */
96 | public function refresh(bool $forceForever = false): string
97 | {
98 | $this->requireToken();
99 |
100 | $this->setToken(
101 | $token = $this->manager
102 | ->refresh($this->getToken(), $forceForever, array_merge(
103 | $this->getCustomClaims(),
104 | ($prv = $this->getPayload(true)->get('prv')) ? ['prv' => $prv] : []
105 | ))
106 | ->get()
107 | );
108 |
109 | return $token;
110 | }
111 |
112 | /**
113 | * Invalidate a token (add it to the blacklist).
114 | *
115 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
116 | * @return $this
117 | */
118 | public function invalidate(bool $forceForever = false)
119 | {
120 | $this->requireToken();
121 |
122 | $this->manager->invalidate($this->getToken(), $forceForever);
123 |
124 | return $this;
125 | }
126 |
127 | /**
128 | * Alias to get the payload, and as a result checks that
129 | * the token is valid i.e. not expired or blacklisted.
130 | *
131 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
132 | */
133 | public function checkOrFail(): Payload
134 | {
135 | return $this->getPayload();
136 | }
137 |
138 | /**
139 | * Check that the token is valid.
140 | *
141 | * @return bool|\HyperfExt\Jwt\Payload
142 | */
143 | public function check(bool $getPayload = false)
144 | {
145 | try {
146 | $payload = $this->checkOrFail();
147 | } catch (JwtException $e) {
148 | return false;
149 | }
150 |
151 | return $getPayload ? $payload : true;
152 | }
153 |
154 | /**
155 | * Get the token.
156 | */
157 | public function getToken(): ?Token
158 | {
159 | if (empty($token = Context::get(Token::class))) {
160 | try {
161 | $this->parseToken();
162 | $token = Context::get(Token::class);
163 | } catch (JwtException $e) {
164 | $token = null;
165 | }
166 | }
167 |
168 | return $token;
169 | }
170 |
171 | /**
172 | * Parse the token from the request.
173 | *
174 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
175 | * @return $this
176 | */
177 | public function parseToken()
178 | {
179 | if (! $token = $this->getRequestParser()->parseToken($this->request)) {
180 | throw new JwtException('The token could not be parsed from the request');
181 | }
182 |
183 | return $this->setToken($token);
184 | }
185 |
186 | /**
187 | * Get the raw Payload instance.
188 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
189 | */
190 | public function getPayload(bool $ignoreExpired = false): Payload
191 | {
192 | $this->requireToken();
193 |
194 | return $this->manager->decode($this->getToken(), true, $ignoreExpired);
195 | }
196 |
197 | /**
198 | * Convenience method to get a claim value.
199 | *
200 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
201 | * @return mixed
202 | */
203 | public function getClaim(string $claim)
204 | {
205 | return $this->getPayload()->get($claim);
206 | }
207 |
208 | /**
209 | * Create a Payload instance.
210 | */
211 | public function makePayload(JwtSubjectInterface $subject): Payload
212 | {
213 | return $this->getPayloadFactory()->make($this->getClaimsArray($subject));
214 | }
215 |
216 | /**
217 | * Check if the subject model matches the one saved in the token.
218 | *
219 | * @param object|string $model
220 | *
221 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
222 | */
223 | public function checkSubjectModel($model): bool
224 | {
225 | if (($prv = $this->getPayload()->get('prv')) === null) {
226 | return true;
227 | }
228 |
229 | return $this->hashSubjectModel($model) === $prv;
230 | }
231 |
232 | /**
233 | * Set the token.
234 | *
235 | * @param \HyperfExt\Jwt\Token|string $token
236 | *
237 | * @return $this
238 | */
239 | public function setToken($token)
240 | {
241 | Context::set(Token::class, $token instanceof Token ? $token : new Token($token));
242 |
243 | return $this;
244 | }
245 |
246 | /**
247 | * Unset the current token.
248 | *
249 | * @return $this
250 | */
251 | public function unsetToken()
252 | {
253 | Context::destroy(Token::class);
254 |
255 | return $this;
256 | }
257 |
258 | /**
259 | * @return $this
260 | */
261 | public function setRequest(ServerRequestInterface $request)
262 | {
263 | $this->request = $request;
264 |
265 | return $this;
266 | }
267 |
268 | /**
269 | * Set whether the subject should be "locked".
270 | *
271 | * @return $this
272 | */
273 | public function setLockSubject(bool $lock)
274 | {
275 | $this->lockSubject = $lock;
276 |
277 | return $this;
278 | }
279 |
280 | /**
281 | * Get the Manager instance.
282 | */
283 | public function getManager(): Manager
284 | {
285 | return $this->manager;
286 | }
287 |
288 | /**
289 | * Get the Parser instance.
290 | */
291 | public function getRequestParser(): RequestParserInterface
292 | {
293 | return $this->requestParser;
294 | }
295 |
296 | /**
297 | * Get the Payload Factory.
298 | */
299 | public function getPayloadFactory(): PayloadFactory
300 | {
301 | return $this->manager->getPayloadFactory();
302 | }
303 |
304 | /**
305 | * Get the Blacklist.
306 | */
307 | public function getBlacklist(): Blacklist
308 | {
309 | return $this->manager->getBlacklist();
310 | }
311 |
312 | /**
313 | * Build the claims array and return it.
314 | */
315 | protected function getClaimsArray(JwtSubjectInterface $subject): array
316 | {
317 | return array_merge(
318 | $this->getClaimsForSubject($subject),
319 | $subject->getJwtCustomClaims(), // custom claims from JwtSubject method
320 | $this->customClaims // custom claims from inline setter
321 | );
322 | }
323 |
324 | /**
325 | * Get the claims associated with a given subject.
326 | */
327 | protected function getClaimsForSubject(JwtSubjectInterface $subject): array
328 | {
329 | return array_merge([
330 | 'sub' => $subject->getJwtIdentifier(),
331 | ], $this->lockSubject ? ['prv' => $this->hashSubjectModel($subject)] : []);
332 | }
333 |
334 | /**
335 | * Hash the subject model and return it.
336 | *
337 | * @param object|string $model
338 | */
339 | protected function hashSubjectModel($model): string
340 | {
341 | return sha1(is_object($model) ? get_class($model) : (string) $model);
342 | }
343 |
344 | /**
345 | * Ensure that a token is available.
346 | *
347 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
348 | */
349 | protected function requireToken()
350 | {
351 | if (! $this->getToken()) {
352 | throw new JwtException('A token is required');
353 | }
354 | }
355 | }
356 |
--------------------------------------------------------------------------------
/src/JwtFactory.php:
--------------------------------------------------------------------------------
1 | lockSubject = (bool) $config->get('jwt.lock_subject');
23 | }
24 |
25 | public function make(): Jwt
26 | {
27 | return make(Jwt::class)->setLockSubject($this->lockSubject);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Manager.php:
--------------------------------------------------------------------------------
1 | codec = $codec;
71 | $this->blacklist = $blacklist;
72 | $this->claimFactory = $claimFactory;
73 | $this->payloadFactory = $payloadFactory;
74 | }
75 |
76 | /**
77 | * Encode a Payload and return the Token.
78 | */
79 | public function encode(Payload $payload): Token
80 | {
81 | $token = $this->codec->encode($payload->get());
82 |
83 | return new Token($token);
84 | }
85 |
86 | /**
87 | * Decode a Token and return the Payload.
88 | *
89 | * @throws \HyperfExt\Jwt\Exceptions\TokenBlacklistedException
90 | */
91 | public function decode(Token $token, bool $checkBlacklist = true, bool $ignoreExpired = false): Payload
92 | {
93 | $payload = $this->payloadFactory->make($this->codec->decode($token->get()), $ignoreExpired);
94 |
95 | if ($checkBlacklist and $this->blacklistEnabled and $this->blacklist->has($payload)) {
96 | throw new TokenBlacklistedException('The token has been blacklisted');
97 | }
98 |
99 | return $payload;
100 | }
101 |
102 | /**
103 | * Refresh a Token and return a new Token.
104 | *
105 | * @throws \HyperfExt\Jwt\Exceptions\TokenBlacklistedException
106 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
107 | */
108 | public function refresh(Token $token, bool $forceForever = false, array $customClaims = []): Token
109 | {
110 | $claims = $this->buildRefreshClaims($this->decode($token, true, true));
111 |
112 | if ($this->blacklistEnabled) {
113 | // Invalidate old token
114 | $this->invalidate($token, $forceForever);
115 | }
116 |
117 | $claims = array_merge($claims, $customClaims);
118 |
119 | // Return the new token
120 | return $this->encode($this->payloadFactory->make($claims));
121 | }
122 |
123 | /**
124 | * Invalidate a Token by adding it to the blacklist.
125 | *
126 | * @throws \HyperfExt\Jwt\Exceptions\JwtException
127 | */
128 | public function invalidate(Token $token, bool $forceForever = false): bool
129 | {
130 | if (! $this->blacklistEnabled) {
131 | throw new JwtException('You must have the blacklist enabled to invalidate a token.');
132 | }
133 |
134 | return call_user_func(
135 | [$this->blacklist, $forceForever ? 'addForever' : 'add'],
136 | $this->decode($token, false, true)
137 | );
138 | }
139 |
140 | /**
141 | * Get the Claim Factory instance.
142 | */
143 | public function getClaimFactory(): ClaimFactory
144 | {
145 | return $this->claimFactory;
146 | }
147 |
148 | /**
149 | * Get the Payload Factory instance.
150 | */
151 | public function getPayloadFactory(): PayloadFactory
152 | {
153 | return $this->payloadFactory;
154 | }
155 |
156 | /**
157 | * Get the JWT codec instance.
158 | */
159 | public function getCodec(): CodecInterface
160 | {
161 | return $this->codec;
162 | }
163 |
164 | /**
165 | * Get the Blacklist instance.
166 | */
167 | public function getBlacklist(): Blacklist
168 | {
169 | return $this->blacklist;
170 | }
171 |
172 | /**
173 | * Set whether the blacklist is enabled.
174 | *
175 | * @return $this
176 | */
177 | public function setBlacklistEnabled(bool $enabled)
178 | {
179 | $this->blacklistEnabled = $enabled;
180 |
181 | return $this;
182 | }
183 |
184 | /**
185 | * Set the claims to be persisted when refreshing a token.
186 | *
187 | * @return $this
188 | */
189 | public function setPersistentClaims(array $claims)
190 | {
191 | $this->persistentClaims = $claims;
192 |
193 | return $this;
194 | }
195 |
196 | /**
197 | * Get the claims to be persisted when refreshing a token.
198 | */
199 | public function getPersistentClaims(): array
200 | {
201 | return $this->persistentClaims;
202 | }
203 |
204 | /**
205 | * Build the claims to go into the refreshed token.
206 | *
207 | * @param \HyperfExt\Jwt\Payload $payload
208 | *
209 | * @return array
210 | */
211 | protected function buildRefreshClaims(Payload $payload)
212 | {
213 | // Get the claims to be persisted from the payload
214 | $persistentClaims = Arr::only($payload->toArray(), $this->persistentClaims);
215 |
216 | // persist the relevant claims
217 | return array_merge(
218 | $persistentClaims,
219 | [
220 | 'sub' => $payload['sub'],
221 | 'iat' => $payload['iat'],
222 | ]
223 | );
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/ManagerFactory.php:
--------------------------------------------------------------------------------
1 | get(ConfigInterface::class)->get('jwt');
30 | if (empty($config)) {
31 | throw new InvalidConfigException(sprintf('JWT config is not defined.'));
32 | }
33 |
34 | $this->config = $config;
35 |
36 | $codec = $this->resolveCodec();
37 | $blacklist = $this->resolveBlacklist();
38 | $claimFactory = $this->resolverClaimFactory();
39 | $payloadFactory = $this->resolverPayloadFactory($claimFactory);
40 |
41 | return make(Manager::class, compact('codec', 'blacklist', 'claimFactory', 'payloadFactory'))
42 | ->setBlacklistEnabled($this->config['blacklist_enabled']);
43 | }
44 |
45 | private function resolveCodec(): CodecInterface
46 | {
47 | $secret = base64_decode($this->config['secret'] ?? '');
48 | $algo = $this->config['algo'] ?? 'HS256';
49 | $keys = $this->config['keys'] ?? [];
50 | if (! empty($keys)) {
51 | $keys['passphrase'] = empty($keys['passphrase']) ? null : base64_decode($keys['passphrase']);
52 | }
53 | return make(Codec::class, compact('secret', 'algo', 'keys'));
54 | }
55 |
56 | private function resolveBlacklist(): Blacklist
57 | {
58 | $storageClass = $this->config['blacklist_storage'] ?? HyperfCache::class;
59 | $storage = make($storageClass, [
60 | 'tag' => 'jwt.default',
61 | ]);
62 | $gracePeriod = $this->config['blacklist_grace_period'];
63 | $refreshTtl = $this->config['refresh_ttl'];
64 |
65 | return make(Blacklist::class, compact('storage', 'gracePeriod', 'refreshTtl'));
66 | }
67 |
68 | private function resolverClaimFactory(): ClaimFactory
69 | {
70 | $ttl = $this->config['ttl'];
71 | $refreshTtl = $this->config['refresh_ttl'];
72 | $leeway = $this->config['leeway'];
73 |
74 | return make(ClaimFactory::class, compact('ttl', 'refreshTtl', 'leeway'));
75 | }
76 |
77 | private function resolverPayloadFactory(ClaimFactory $claimFactory): PayloadFactory
78 | {
79 | return make(PayloadFactory::class, compact('claimFactory'))
80 | ->setTtl($this->config['ttl']);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Payload.php:
--------------------------------------------------------------------------------
1 | validator = ApplicationContext::getContainer()->get(PayloadValidatorInterface::class);
46 | $this->claims = $this->validator->check($claims, $ignoreExpired);
47 | }
48 |
49 | /**
50 | * Get the payload as a string.
51 | */
52 | public function __toString(): string
53 | {
54 | return $this->toJson();
55 | }
56 |
57 | /**
58 | * Invoke the Payload as a callable function.
59 | *
60 | * @param mixed $claim
61 | *
62 | * @return mixed
63 | */
64 | public function __invoke($claim = null)
65 | {
66 | return $this->get($claim);
67 | }
68 |
69 | /**
70 | * Magically get a claim value.
71 | *
72 | * @throws \BadMethodCallException
73 | * @return mixed
74 | */
75 | public function __call(string $method, array $parameters)
76 | {
77 | if (preg_match('/get(.+)\b/i', $method, $matches)) {
78 | foreach ($this->claims as $claim) {
79 | if (get_class($claim) === 'HyperfExt\\Jwt\\Claims\\' . $matches[1]) {
80 | return $claim->getValue();
81 | }
82 | }
83 | }
84 |
85 | throw new BadMethodCallException(sprintf('The claim [%s] does not exist on the payload.', $method));
86 | }
87 |
88 | /**
89 | * Get the array of claim instances.
90 | */
91 | public function getClaims(): Collection
92 | {
93 | return $this->claims;
94 | }
95 |
96 | /**
97 | * Checks if a payload matches some expected values.
98 | */
99 | public function matches(array $values, bool $strict = false): bool
100 | {
101 | if (empty($values)) {
102 | return false;
103 | }
104 |
105 | $claims = $this->getClaims();
106 |
107 | foreach ($values as $key => $value) {
108 | if (! $claims->has($key) or ! $claims->get($key)->matches($value, $strict)) {
109 | return false;
110 | }
111 | }
112 |
113 | return true;
114 | }
115 |
116 | /**
117 | * Checks if a payload strictly matches some expected values.
118 | */
119 | public function matchesStrict(array $values): bool
120 | {
121 | return $this->matches($values, true);
122 | }
123 |
124 | /**
125 | * Get the payload.
126 | *
127 | * @param mixed $claim
128 | *
129 | * @return mixed
130 | */
131 | public function get($claim = null)
132 | {
133 | $claim = value($claim);
134 |
135 | if ($claim !== null) {
136 | if (is_array($claim)) {
137 | return array_map([$this, 'get'], $claim);
138 | }
139 |
140 | return Arr::get($this->toArray(), $claim);
141 | }
142 |
143 | return $this->toArray();
144 | }
145 |
146 | /**
147 | * Get the underlying Claim instance.
148 | */
149 | public function getInternal(string $claim): AbstractClaim
150 | {
151 | return $this->claims->getByClaimName($claim);
152 | }
153 |
154 | /**
155 | * Determine whether the payload has the claim (by instance).
156 | */
157 | public function has(AbstractClaim $claim): bool
158 | {
159 | return $this->claims->has($claim->getName());
160 | }
161 |
162 | /**
163 | * Determine whether the payload has the claim (by key).
164 | */
165 | public function hasKey(string $claim): bool
166 | {
167 | return $this->offsetExists($claim);
168 | }
169 |
170 | /**
171 | * Get the array of claims.
172 | */
173 | public function toArray(): array
174 | {
175 | return $this->claims->toPlainArray();
176 | }
177 |
178 | /**
179 | * Convert the object into something JSON serializable.
180 | */
181 | public function jsonSerialize(): array
182 | {
183 | return $this->toArray();
184 | }
185 |
186 | /**
187 | * Get the payload as JSON.
188 | */
189 | public function toJson(int $options = JSON_UNESCAPED_SLASHES): string
190 | {
191 | return json_encode($this->toArray(), $options);
192 | }
193 |
194 | /**
195 | * Determine if an item exists at an offset.
196 | *
197 | * @param mixed $key
198 | */
199 | public function offsetExists($key): bool
200 | {
201 | return Arr::has($this->toArray(), $key);
202 | }
203 |
204 | /**
205 | * Get an item at a given offset.
206 | *
207 | * @param mixed $key
208 | *
209 | * @return mixed
210 | */
211 | public function offsetGet($key)
212 | {
213 | return Arr::get($this->toArray(), $key);
214 | }
215 |
216 | /**
217 | * Don't allow changing the payload as it should be immutable.
218 | *
219 | * @param mixed $key
220 | * @param mixed $value
221 | *
222 | * @throws \HyperfExt\Jwt\Exceptions\PayloadException
223 | */
224 | public function offsetSet($key, $value)
225 | {
226 | throw new PayloadException('The payload is immutable');
227 | }
228 |
229 | /**
230 | * Don't allow changing the payload as it should be immutable.
231 | *
232 | * @param string $key
233 | *
234 | * @throws \HyperfExt\Jwt\Exceptions\PayloadException
235 | */
236 | public function offsetUnset($key)
237 | {
238 | throw new PayloadException('The payload is immutable');
239 | }
240 |
241 | /**
242 | * Count the number of claims.
243 | */
244 | public function count(): int
245 | {
246 | return count($this->toArray());
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/PayloadFactory.php:
--------------------------------------------------------------------------------
1 | claimFactory = $claimFactory;
42 | }
43 |
44 | /**
45 | * Create the Payload instance.
46 | */
47 | public function make(array $claims, bool $ignoreExpired = false): Payload
48 | {
49 | return new Payload($this->resolveClaims($this->buildClaims($claims)), $ignoreExpired);
50 | }
51 |
52 | /**
53 | * Set the default claims to be added to the Payload.
54 | *
55 | * @return $this
56 | */
57 | public function setDefaultClaims(array $claims)
58 | {
59 | $this->defaultClaims = $claims;
60 |
61 | return $this;
62 | }
63 |
64 | /**
65 | * Get the default claims.
66 | *
67 | * @return string[]
68 | */
69 | public function getDefaultClaims(): array
70 | {
71 | return $this->defaultClaims;
72 | }
73 |
74 | /**
75 | * Helper to set the ttl.
76 | *
77 | * @return $this
78 | */
79 | public function setTtl(int $ttl)
80 | {
81 | $this->claimFactory->setTtl($ttl);
82 |
83 | return $this;
84 | }
85 |
86 | /**
87 | * Helper to get the ttl.
88 | */
89 | public function getTtl(): int
90 | {
91 | return $this->claimFactory->getTtl();
92 | }
93 |
94 | /**
95 | * Build the default claims.
96 | */
97 | protected function buildClaims(array $claims): Collection
98 | {
99 | $collection = new Collection();
100 | $defaultClaims = $this->getDefaultClaims();
101 |
102 | // remove the exp claim if it exists and the ttl is null
103 | if ($this->claimFactory->getTtl() === null and $key = array_search('exp', $defaultClaims)) {
104 | unset($defaultClaims[$key]);
105 | }
106 |
107 | // add the default claims
108 | foreach ($defaultClaims as $claim) {
109 | $collection->put($claim, $this->claimFactory->make($claim));
110 | }
111 |
112 | // add custom claims on top, allowing them to overwrite defaults
113 | foreach ($claims as $name => $value) {
114 | $collection->put($name, $value);
115 | }
116 |
117 | return $collection;
118 | }
119 |
120 | /**
121 | * Build out the Claim DTO's.
122 | */
123 | protected function resolveClaims(Collection $claims): Collection
124 | {
125 | return $claims->map(function ($value, $name) {
126 | return $value instanceof ClaimInterface ? $value : $this->claimFactory->get($name, $value);
127 | });
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/AuthHeaders.php:
--------------------------------------------------------------------------------
1 | getHeaderLine($this->header);
35 |
36 | if ($header and preg_match('/' . $this->prefix . '\s*(\S+)\b/i', $header, $matches)) {
37 | return $matches[1];
38 | }
39 |
40 | return null;
41 | }
42 |
43 | /**
44 | * Set the header name.
45 | *
46 | * @return $this
47 | */
48 | public function setHeaderName(string $headerName)
49 | {
50 | $this->header = $headerName;
51 |
52 | return $this;
53 | }
54 |
55 | /**
56 | * Set the header prefix.
57 | *
58 | * @return $this
59 | */
60 | public function setHeaderPrefix(string $headerPrefix)
61 | {
62 | $this->prefix = $headerPrefix;
63 |
64 | return $this;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/Cookies.php:
--------------------------------------------------------------------------------
1 | getCookieParams(), $this->key);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/InputSource.php:
--------------------------------------------------------------------------------
1 | getParsedBody()) ? $data : [],
24 | $this->key
25 | );
26 | return empty($data) === null ? null : (string) $data;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/KeyTrait.php:
--------------------------------------------------------------------------------
1 | key = $key;
32 |
33 | return $this;
34 | }
35 |
36 | /**
37 | * Get the key.
38 | *
39 | * @return string
40 | */
41 | public function getKey()
42 | {
43 | return $this->key;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/QueryString.php:
--------------------------------------------------------------------------------
1 | getQueryParams(), $this->key);
23 | return empty($data) === null ? null : (string) $data;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/RequestParser/Handlers/RouteParams.php:
--------------------------------------------------------------------------------
1 | route($this->key);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/RequestParser/RequestParser.php:
--------------------------------------------------------------------------------
1 | handlers = $handlers;
29 | }
30 |
31 | public function getHandlers(): array
32 | {
33 | return $this->handlers;
34 | }
35 |
36 | public function setHandlers(array $handlers)
37 | {
38 | $this->handlers = $handlers;
39 |
40 | return $this;
41 | }
42 |
43 | public function parseToken(ServerRequestInterface $request): ?string
44 | {
45 | foreach ($this->handlers as $handler) {
46 | if ($token = $handler->parse($request)) {
47 | return $token;
48 | }
49 | }
50 | return null;
51 | }
52 |
53 | public function hasToken(ServerRequestInterface $request): bool
54 | {
55 | return $this->parseToken($request) !== null;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/RequestParser/RequestParserFactory.php:
--------------------------------------------------------------------------------
1 | setHandlers([
24 | new AuthHeaders(),
25 | new QueryString(),
26 | new InputSource(),
27 | new RouteParams(),
28 | new Cookies(),
29 | ]);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Storage/HyperfCache.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
38 | $this->tag = $tag;
39 | }
40 |
41 | public function add(string $key, $value, int $ttl)
42 | {
43 | $this->cache->set($this->resolveKey($key), $value, $ttl);
44 | }
45 |
46 | public function forever(string $key, $value)
47 | {
48 | $this->cache->set($this->resolveKey($key), $value);
49 | }
50 |
51 | public function get(string $key)
52 | {
53 | return $this->cache->get($this->resolveKey($key));
54 | }
55 |
56 | public function destroy(string $key): bool
57 | {
58 | return $this->cache->delete($this->resolveKey($key));
59 | }
60 |
61 | public function flush(): void
62 | {
63 | method_exists($cache = $this->cache, 'clearPrefix')
64 | ? $cache->clearPrefix($this->tag)
65 | : $cache->clear();
66 | }
67 |
68 | protected function cache(): CacheInterface
69 | {
70 | return $this->cache;
71 | }
72 |
73 | protected function resolveKey(string $key)
74 | {
75 | return $this->tag . '.' . $key;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Token.php:
--------------------------------------------------------------------------------
1 | validator = ApplicationContext::getContainer()->get(TokenValidatorInterface::class);
34 | $this->value = (string) $this->validator->check($value);
35 | }
36 |
37 | /**
38 | * Get the token when casting to string.
39 | */
40 | public function __toString(): string
41 | {
42 | return $this->get();
43 | }
44 |
45 | /**
46 | * Get the token.
47 | */
48 | public function get(): string
49 | {
50 | return $this->value;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Utils.php:
--------------------------------------------------------------------------------
1 | timezone('UTC');
31 | }
32 |
33 | /**
34 | * Checks if a timestamp is in the past.
35 | */
36 | public static function isPast(int $timestamp, int $leeway = 0): bool
37 | {
38 | $timestamp = static::timestamp($timestamp);
39 |
40 | return $leeway > 0
41 | ? $timestamp->addSeconds($leeway)->isPast()
42 | : $timestamp->isPast();
43 | }
44 |
45 | /**
46 | * Checks if a timestamp is in the future.
47 | */
48 | public static function isFuture(int $timestamp, int $leeway = 0): bool
49 | {
50 | $timestamp = static::timestamp($timestamp);
51 |
52 | return $leeway > 0
53 | ? $timestamp->subSeconds($leeway)->isFuture()
54 | : $timestamp->isFuture();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Validators/PayloadValidator.php:
--------------------------------------------------------------------------------
1 | setRequiredClaims($config->get('jwt.required_claims', []));
31 | }
32 |
33 | public function check(Collection $value, bool $ignoreExpired = false): Collection
34 | {
35 | $this->validateStructure($value);
36 |
37 | return $this->validatePayload($value, $ignoreExpired);
38 | }
39 |
40 | public function isValid(Collection $value, bool $ignoreExpired = false): bool
41 | {
42 | try {
43 | $this->check($value, $ignoreExpired);
44 | } catch (JwtException $e) {
45 | return false;
46 | }
47 |
48 | return true;
49 | }
50 |
51 | /**
52 | * Set the required claims.
53 | *
54 | * @return $this
55 | */
56 | public function setRequiredClaims(array $claims)
57 | {
58 | $this->requiredClaims = $claims;
59 |
60 | return $this;
61 | }
62 |
63 | /**
64 | * Ensure the payload contains the required claims and
65 | * the claims have the relevant type.
66 | *
67 | * @throws \HyperfExt\Jwt\Exceptions\TokenInvalidException
68 | */
69 | protected function validateStructure(Collection $claims)
70 | {
71 | if ($this->requiredClaims and ! $claims->hasAllClaims($this->requiredClaims)) {
72 | throw new TokenInvalidException('JWT payload does not contain the required claims');
73 | }
74 | return $this;
75 | }
76 |
77 | /**
78 | * Validate the payload timestamps.
79 | *
80 | * @throws \HyperfExt\Jwt\Exceptions\TokenExpiredException
81 | * @throws \HyperfExt\Jwt\Exceptions\TokenInvalidException
82 | */
83 | protected function validatePayload(Collection $claims, bool $ignoreExpired = false): Collection
84 | {
85 | return $claims->validate($ignoreExpired);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Validators/TokenValidator.php:
--------------------------------------------------------------------------------
1 | validateStructure($value);
25 | return $value;
26 | }
27 |
28 | /**
29 | * Helper function to return a boolean.
30 | */
31 | public function isValid(string $value): bool
32 | {
33 | try {
34 | $this->check($value);
35 | } catch (JwtException $e) {
36 | return false;
37 | }
38 |
39 | return true;
40 | }
41 |
42 | /**
43 | * @throws \HyperfExt\Jwt\Exceptions\TokenInvalidException
44 | */
45 | protected function validateStructure(string $token)
46 | {
47 | $parts = explode('.', $token);
48 |
49 | if (count($parts) !== 3) {
50 | throw new TokenInvalidException('Wrong number of segments');
51 | }
52 |
53 | $parts = array_filter(array_map('trim', $parts));
54 |
55 | if (count($parts) !== 3 or implode('.', $parts) !== $token) {
56 | throw new TokenInvalidException('Malformed token');
57 | }
58 |
59 | return $this;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------