├── .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 | --------------------------------------------------------------------------------