├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── BeforeValidException.php ├── CachedKeySet.php ├── ExpiredException.php ├── JWK.php ├── JWT.php ├── JWTExceptionWithPayloadInterface.php ├── Key.php └── SignatureInvalidException.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [6.11.1](https://github.com/firebase/php-jwt/compare/v6.11.0...v6.11.1) (2025-04-09) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * update error text for consistency ([#528](https://github.com/firebase/php-jwt/issues/528)) ([c11113a](https://github.com/firebase/php-jwt/commit/c11113afa13265e016a669e75494b9203b8a7775)) 9 | 10 | ## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23) 11 | 12 | 13 | ### Features 14 | 15 | * support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca)) 21 | 22 | ## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5)) 28 | * support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e)) 29 | 30 | ## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648)) 36 | * ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae)) 37 | 38 | ## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28) 39 | 40 | 41 | ### Features 42 | 43 | * allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9)) 44 | 45 | ## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04) 46 | 47 | 48 | ### Features 49 | 50 | * add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2)) 51 | 52 | ## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e)) 58 | * different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4)) 59 | 60 | ## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14) 61 | 62 | 63 | ### Features 64 | 65 | * add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d)) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718)) 71 | 72 | ## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14) 73 | 74 | 75 | ### Features 76 | 77 | * add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a)) 78 | 79 | ## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13) 80 | 81 | 82 | ### Features 83 | 84 | * allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3)) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797)) 90 | 91 | ## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a)) 97 | 98 | 99 | ### Miscellaneous Chores 100 | 101 | * drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495)) 102 | 103 | ## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08) 104 | 105 | 106 | ### Features 107 | 108 | * add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95)) 109 | * improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c)) 110 | 111 | ## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd)) 117 | 118 | ## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22)) 124 | * string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2)) 125 | 126 | ## 6.3.0 / 2022-07-15 127 | 128 | - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) 129 | - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435)) 130 | 131 | ## 6.2.0 / 2022-05-14 132 | 133 | - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) 134 | - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). 135 | 136 | ## 6.1.0 / 2022-03-23 137 | 138 | - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 139 | - Add parameter typing and return types where possible 140 | 141 | ## 6.0.0 / 2022-01-24 142 | 143 | - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. 144 | - New Key object to prevent key/algorithm type confusion (#365) 145 | - Add JWK support (#273) 146 | - Add ES256 support (#256) 147 | - Add ES384 support (#324) 148 | - Add Ed25519 support (#343) 149 | 150 | ## 5.0.0 / 2017-06-26 151 | - Support RS384 and RS512. 152 | See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! 153 | - Add an example for RS256 openssl. 154 | See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! 155 | - Detect invalid Base64 encoding in signature. 156 | See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! 157 | - Update `JWT::verify` to handle OpenSSL errors. 158 | See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! 159 | - Add `array` type hinting to `decode` method 160 | See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! 161 | - Add all JSON error types. 162 | See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! 163 | - Bugfix 'kid' not in given key list. 164 | See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! 165 | - Miscellaneous cleanup, documentation and test fixes. 166 | See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), 167 | [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and 168 | [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), 169 | [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! 170 | 171 | ## 4.0.0 / 2016-07-17 172 | - Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! 173 | - Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! 174 | - Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! 175 | - Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! 176 | 177 | ## 3.0.0 / 2015-07-22 178 | - Minimum PHP version updated from `5.2.0` to `5.3.0`. 179 | - Add `\Firebase\JWT` namespace. See 180 | [#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to 181 | [@Dashron](https://github.com/Dashron)! 182 | - Require a non-empty key to decode and verify a JWT. See 183 | [#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to 184 | [@sjones608](https://github.com/sjones608)! 185 | - Cleaner documentation blocks in the code. See 186 | [#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to 187 | [@johanderuijter](https://github.com/johanderuijter)! 188 | 189 | ## 2.2.0 / 2015-06-22 190 | - Add support for adding custom, optional JWT headers to `JWT::encode()`. See 191 | [#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to 192 | [@mcocaro](https://github.com/mcocaro)! 193 | 194 | ## 2.1.0 / 2015-05-20 195 | - Add support for adding a leeway to `JWT:decode()` that accounts for clock skew 196 | between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! 197 | - Add support for passing an object implementing the `ArrayAccess` interface for 198 | `$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! 199 | 200 | ## 2.0.0 / 2015-04-01 201 | - **Note**: It is strongly recommended that you update to > v2.0.0 to address 202 | known security vulnerabilities in prior versions when both symmetric and 203 | asymmetric keys are used together. 204 | - Update signature for `JWT::decode(...)` to require an array of supported 205 | algorithms to use when verifying token signatures. 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Neuman Vong 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) 2 | [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) 3 | [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) 4 | [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) 5 | 6 | PHP-JWT 7 | ======= 8 | A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519). 9 | 10 | Installation 11 | ------------ 12 | 13 | Use composer to manage your dependencies and download PHP-JWT: 14 | 15 | ```bash 16 | composer require firebase/php-jwt 17 | ``` 18 | 19 | Optionally, install the `paragonie/sodium_compat` package from composer if your 20 | php env does not have libsodium installed: 21 | 22 | ```bash 23 | composer require paragonie/sodium_compat 24 | ``` 25 | 26 | Example 27 | ------- 28 | ```php 29 | use Firebase\JWT\JWT; 30 | use Firebase\JWT\Key; 31 | 32 | $key = 'example_key'; 33 | $payload = [ 34 | 'iss' => 'http://example.org', 35 | 'aud' => 'http://example.com', 36 | 'iat' => 1356999524, 37 | 'nbf' => 1357000000 38 | ]; 39 | 40 | /** 41 | * IMPORTANT: 42 | * You must specify supported algorithms for your application. See 43 | * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 44 | * for a list of spec-compliant algorithms. 45 | */ 46 | $jwt = JWT::encode($payload, $key, 'HS256'); 47 | $decoded = JWT::decode($jwt, new Key($key, 'HS256')); 48 | print_r($decoded); 49 | 50 | // Pass a stdClass in as the third parameter to get the decoded header values 51 | $headers = new stdClass(); 52 | $decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers); 53 | print_r($headers); 54 | 55 | /* 56 | NOTE: This will now be an object instead of an associative array. To get 57 | an associative array, you will need to cast it as such: 58 | */ 59 | 60 | $decoded_array = (array) $decoded; 61 | 62 | /** 63 | * You can add a leeway to account for when there is a clock skew times between 64 | * the signing and verifying servers. It is recommended that this leeway should 65 | * not be bigger than a few minutes. 66 | * 67 | * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef 68 | */ 69 | JWT::$leeway = 60; // $leeway in seconds 70 | $decoded = JWT::decode($jwt, new Key($key, 'HS256')); 71 | ``` 72 | Example encode/decode headers 73 | ------- 74 | Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by 75 | this library. This is because without verifying the JWT, the header values could have been tampered with. 76 | Any value pulled from an unverified header should be treated as if it could be any string sent in from an 77 | attacker. If this is something you still want to do in your application for whatever reason, it's possible to 78 | decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT 79 | header part: 80 | ```php 81 | use Firebase\JWT\JWT; 82 | 83 | $key = 'example_key'; 84 | $payload = [ 85 | 'iss' => 'http://example.org', 86 | 'aud' => 'http://example.com', 87 | 'iat' => 1356999524, 88 | 'nbf' => 1357000000 89 | ]; 90 | 91 | $headers = [ 92 | 'x-forwarded-for' => 'www.google.com' 93 | ]; 94 | 95 | // Encode headers in the JWT string 96 | $jwt = JWT::encode($payload, $key, 'HS256', null, $headers); 97 | 98 | // Decode headers from the JWT string WITHOUT validation 99 | // **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified. 100 | // These headers could be any value sent by an attacker. 101 | list($headersB64, $payloadB64, $sig) = explode('.', $jwt); 102 | $decoded = json_decode(base64_decode($headersB64), true); 103 | 104 | print_r($decoded); 105 | ``` 106 | Example with RS256 (openssl) 107 | ---------------------------- 108 | ```php 109 | use Firebase\JWT\JWT; 110 | use Firebase\JWT\Key; 111 | 112 | $privateKey = << 'example.org', 156 | 'aud' => 'example.com', 157 | 'iat' => 1356999524, 158 | 'nbf' => 1357000000 159 | ]; 160 | 161 | $jwt = JWT::encode($payload, $privateKey, 'RS256'); 162 | echo "Encode:\n" . print_r($jwt, true) . "\n"; 163 | 164 | $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); 165 | 166 | /* 167 | NOTE: This will now be an object instead of an associative array. To get 168 | an associative array, you will need to cast it as such: 169 | */ 170 | 171 | $decoded_array = (array) $decoded; 172 | echo "Decode:\n" . print_r($decoded_array, true) . "\n"; 173 | ``` 174 | 175 | Example with a passphrase 176 | ------------------------- 177 | 178 | ```php 179 | use Firebase\JWT\JWT; 180 | use Firebase\JWT\Key; 181 | 182 | // Your passphrase 183 | $passphrase = '[YOUR_PASSPHRASE]'; 184 | 185 | // Your private key file with passphrase 186 | // Can be generated with "ssh-keygen -t rsa -m pem" 187 | $privateKeyFile = '/path/to/key-with-passphrase.pem'; 188 | 189 | // Create a private key of type "resource" 190 | $privateKey = openssl_pkey_get_private( 191 | file_get_contents($privateKeyFile), 192 | $passphrase 193 | ); 194 | 195 | $payload = [ 196 | 'iss' => 'example.org', 197 | 'aud' => 'example.com', 198 | 'iat' => 1356999524, 199 | 'nbf' => 1357000000 200 | ]; 201 | 202 | $jwt = JWT::encode($payload, $privateKey, 'RS256'); 203 | echo "Encode:\n" . print_r($jwt, true) . "\n"; 204 | 205 | // Get public key from the private key, or pull from from a file. 206 | $publicKey = openssl_pkey_get_details($privateKey)['key']; 207 | 208 | $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); 209 | echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; 210 | ``` 211 | 212 | Example with EdDSA (libsodium and Ed25519 signature) 213 | ---------------------------- 214 | ```php 215 | use Firebase\JWT\JWT; 216 | use Firebase\JWT\Key; 217 | 218 | // Public and private keys are expected to be Base64 encoded. The last 219 | // non-empty line is used so that keys can be generated with 220 | // sodium_crypto_sign_keypair(). The secret keys generated by other tools may 221 | // need to be adjusted to match the input expected by libsodium. 222 | 223 | $keyPair = sodium_crypto_sign_keypair(); 224 | 225 | $privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); 226 | 227 | $publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); 228 | 229 | $payload = [ 230 | 'iss' => 'example.org', 231 | 'aud' => 'example.com', 232 | 'iat' => 1356999524, 233 | 'nbf' => 1357000000 234 | ]; 235 | 236 | $jwt = JWT::encode($payload, $privateKey, 'EdDSA'); 237 | echo "Encode:\n" . print_r($jwt, true) . "\n"; 238 | 239 | $decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); 240 | echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; 241 | ```` 242 | 243 | Example with multiple keys 244 | -------------------------- 245 | ```php 246 | use Firebase\JWT\JWT; 247 | use Firebase\JWT\Key; 248 | 249 | // Example RSA keys from previous example 250 | // $privateKey1 = '...'; 251 | // $publicKey1 = '...'; 252 | 253 | // Example EdDSA keys from previous example 254 | // $privateKey2 = '...'; 255 | // $publicKey2 = '...'; 256 | 257 | $payload = [ 258 | 'iss' => 'example.org', 259 | 'aud' => 'example.com', 260 | 'iat' => 1356999524, 261 | 'nbf' => 1357000000 262 | ]; 263 | 264 | $jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1'); 265 | $jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2'); 266 | echo "Encode 1:\n" . print_r($jwt1, true) . "\n"; 267 | echo "Encode 2:\n" . print_r($jwt2, true) . "\n"; 268 | 269 | $keys = [ 270 | 'kid1' => new Key($publicKey1, 'RS256'), 271 | 'kid2' => new Key($publicKey2, 'EdDSA'), 272 | ]; 273 | 274 | $decoded1 = JWT::decode($jwt1, $keys); 275 | $decoded2 = JWT::decode($jwt2, $keys); 276 | 277 | echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n"; 278 | echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n"; 279 | ``` 280 | 281 | Using JWKs 282 | ---------- 283 | 284 | ```php 285 | use Firebase\JWT\JWK; 286 | use Firebase\JWT\JWT; 287 | 288 | // Set of keys. The "keys" key is required. For example, the JSON response to 289 | // this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk 290 | $jwks = ['keys' => []]; 291 | 292 | // JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key 293 | // objects. Pass this as the second parameter to JWT::decode. 294 | JWT::decode($jwt, JWK::parseKeySet($jwks)); 295 | ``` 296 | 297 | Using Cached Key Sets 298 | --------------------- 299 | 300 | The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI. 301 | This has the following advantages: 302 | 303 | 1. The results are cached for performance. 304 | 2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation. 305 | 3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second. 306 | 307 | ```php 308 | use Firebase\JWT\CachedKeySet; 309 | use Firebase\JWT\JWT; 310 | 311 | // The URI for the JWKS you wish to cache the results from 312 | $jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk'; 313 | 314 | // Create an HTTP client (can be any PSR-7 compatible HTTP client) 315 | $httpClient = new GuzzleHttp\Client(); 316 | 317 | // Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory) 318 | $httpFactory = new GuzzleHttp\Psr\HttpFactory(); 319 | 320 | // Create a cache item pool (can be any PSR-6 compatible cache item pool) 321 | $cacheItemPool = Phpfastcache\CacheManager::getInstance('files'); 322 | 323 | $keySet = new CachedKeySet( 324 | $jwksUri, 325 | $httpClient, 326 | $httpFactory, 327 | $cacheItemPool, 328 | null, // $expiresAfter int seconds to set the JWKS to expire 329 | true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys 330 | ); 331 | 332 | $jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above 333 | $decoded = JWT::decode($jwt, $keySet); 334 | ``` 335 | 336 | Miscellaneous 337 | ------------- 338 | 339 | #### Exception Handling 340 | 341 | When a call to `JWT::decode` is invalid, it will throw one of the following exceptions: 342 | 343 | ```php 344 | use Firebase\JWT\JWT; 345 | use Firebase\JWT\SignatureInvalidException; 346 | use Firebase\JWT\BeforeValidException; 347 | use Firebase\JWT\ExpiredException; 348 | use DomainException; 349 | use InvalidArgumentException; 350 | use UnexpectedValueException; 351 | 352 | try { 353 | $decoded = JWT::decode($jwt, $keys); 354 | } catch (InvalidArgumentException $e) { 355 | // provided key/key-array is empty or malformed. 356 | } catch (DomainException $e) { 357 | // provided algorithm is unsupported OR 358 | // provided key is invalid OR 359 | // unknown error thrown in openSSL or libsodium OR 360 | // libsodium is required but not available. 361 | } catch (SignatureInvalidException $e) { 362 | // provided JWT signature verification failed. 363 | } catch (BeforeValidException $e) { 364 | // provided JWT is trying to be used before "nbf" claim OR 365 | // provided JWT is trying to be used before "iat" claim. 366 | } catch (ExpiredException $e) { 367 | // provided JWT is trying to be used after "exp" claim. 368 | } catch (UnexpectedValueException $e) { 369 | // provided JWT is malformed OR 370 | // provided JWT is missing an algorithm / using an unsupported algorithm OR 371 | // provided JWT algorithm does not match provided key OR 372 | // provided key ID in key/key-array is empty or invalid. 373 | } 374 | ``` 375 | 376 | All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified 377 | like this: 378 | 379 | ```php 380 | use Firebase\JWT\JWT; 381 | use UnexpectedValueException; 382 | try { 383 | $decoded = JWT::decode($jwt, $keys); 384 | } catch (LogicException $e) { 385 | // errors having to do with environmental setup or malformed JWT Keys 386 | } catch (UnexpectedValueException $e) { 387 | // errors having to do with JWT signature and claims 388 | } 389 | ``` 390 | 391 | #### Casting to array 392 | 393 | The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays 394 | instead, you can do the following: 395 | 396 | ```php 397 | // return type is stdClass 398 | $decoded = JWT::decode($jwt, $keys); 399 | 400 | // cast to array 401 | $decoded = json_decode(json_encode($decoded), true); 402 | ``` 403 | 404 | Tests 405 | ----- 406 | Run the tests using phpunit: 407 | 408 | ```bash 409 | $ pear install PHPUnit 410 | $ phpunit --configuration phpunit.xml.dist 411 | PHPUnit 3.7.10 by Sebastian Bergmann. 412 | ..... 413 | Time: 0 seconds, Memory: 2.50Mb 414 | OK (5 tests, 5 assertions) 415 | ``` 416 | 417 | New Lines in private keys 418 | ----- 419 | 420 | If your private key contains `\n` characters, be sure to wrap it in double quotes `""` 421 | and not single quotes `''` in order to properly interpret the escaped characters. 422 | 423 | License 424 | ------- 425 | [3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause). 426 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase/php-jwt", 3 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 4 | "homepage": "https://github.com/firebase/php-jwt", 5 | "keywords": [ 6 | "php", 7 | "jwt" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Neuman Vong", 12 | "email": "neuman+pear@twilio.com", 13 | "role": "Developer" 14 | }, 15 | { 16 | "name": "Anant Narayanan", 17 | "email": "anant@php.net", 18 | "role": "Developer" 19 | } 20 | ], 21 | "license": "BSD-3-Clause", 22 | "require": { 23 | "php": "^8.0" 24 | }, 25 | "suggest": { 26 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", 27 | "ext-sodium": "Support EdDSA (Ed25519) signatures" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Firebase\\JWT\\": "src" 32 | } 33 | }, 34 | "require-dev": { 35 | "guzzlehttp/guzzle": "^7.4", 36 | "phpspec/prophecy-phpunit": "^2.0", 37 | "phpunit/phpunit": "^9.5", 38 | "psr/cache": "^2.0||^3.0", 39 | "psr/http-client": "^1.0", 40 | "psr/http-factory": "^1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/BeforeValidException.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 12 | } 13 | 14 | public function getPayload(): object 15 | { 16 | return $this->payload; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/CachedKeySet.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class CachedKeySet implements ArrayAccess 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $jwksUri; 25 | /** 26 | * @var ClientInterface 27 | */ 28 | private $httpClient; 29 | /** 30 | * @var RequestFactoryInterface 31 | */ 32 | private $httpFactory; 33 | /** 34 | * @var CacheItemPoolInterface 35 | */ 36 | private $cache; 37 | /** 38 | * @var ?int 39 | */ 40 | private $expiresAfter; 41 | /** 42 | * @var ?CacheItemInterface 43 | */ 44 | private $cacheItem; 45 | /** 46 | * @var array> 47 | */ 48 | private $keySet; 49 | /** 50 | * @var string 51 | */ 52 | private $cacheKey; 53 | /** 54 | * @var string 55 | */ 56 | private $cacheKeyPrefix = 'jwks'; 57 | /** 58 | * @var int 59 | */ 60 | private $maxKeyLength = 64; 61 | /** 62 | * @var bool 63 | */ 64 | private $rateLimit; 65 | /** 66 | * @var string 67 | */ 68 | private $rateLimitCacheKey; 69 | /** 70 | * @var int 71 | */ 72 | private $maxCallsPerMinute = 10; 73 | /** 74 | * @var string|null 75 | */ 76 | private $defaultAlg; 77 | 78 | public function __construct( 79 | string $jwksUri, 80 | ClientInterface $httpClient, 81 | RequestFactoryInterface $httpFactory, 82 | CacheItemPoolInterface $cache, 83 | ?int $expiresAfter = null, 84 | bool $rateLimit = false, 85 | ?string $defaultAlg = null 86 | ) { 87 | $this->jwksUri = $jwksUri; 88 | $this->httpClient = $httpClient; 89 | $this->httpFactory = $httpFactory; 90 | $this->cache = $cache; 91 | $this->expiresAfter = $expiresAfter; 92 | $this->rateLimit = $rateLimit; 93 | $this->defaultAlg = $defaultAlg; 94 | $this->setCacheKeys(); 95 | } 96 | 97 | /** 98 | * @param string $keyId 99 | * @return Key 100 | */ 101 | public function offsetGet($keyId): Key 102 | { 103 | if (!$this->keyIdExists($keyId)) { 104 | throw new OutOfBoundsException('Key ID not found'); 105 | } 106 | return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); 107 | } 108 | 109 | /** 110 | * @param string $keyId 111 | * @return bool 112 | */ 113 | public function offsetExists($keyId): bool 114 | { 115 | return $this->keyIdExists($keyId); 116 | } 117 | 118 | /** 119 | * @param string $offset 120 | * @param Key $value 121 | */ 122 | public function offsetSet($offset, $value): void 123 | { 124 | throw new LogicException('Method not implemented'); 125 | } 126 | 127 | /** 128 | * @param string $offset 129 | */ 130 | public function offsetUnset($offset): void 131 | { 132 | throw new LogicException('Method not implemented'); 133 | } 134 | 135 | /** 136 | * @return array 137 | */ 138 | private function formatJwksForCache(string $jwks): array 139 | { 140 | $jwks = json_decode($jwks, true); 141 | 142 | if (!isset($jwks['keys'])) { 143 | throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); 144 | } 145 | 146 | if (empty($jwks['keys'])) { 147 | throw new InvalidArgumentException('JWK Set did not contain any keys'); 148 | } 149 | 150 | $keys = []; 151 | foreach ($jwks['keys'] as $k => $v) { 152 | $kid = isset($v['kid']) ? $v['kid'] : $k; 153 | $keys[(string) $kid] = $v; 154 | } 155 | 156 | return $keys; 157 | } 158 | 159 | private function keyIdExists(string $keyId): bool 160 | { 161 | if (null === $this->keySet) { 162 | $item = $this->getCacheItem(); 163 | // Try to load keys from cache 164 | if ($item->isHit()) { 165 | // item found! retrieve it 166 | $this->keySet = $item->get(); 167 | // If the cached item is a string, the JWKS response was cached (previous behavior). 168 | // Parse this into expected format array instead. 169 | if (\is_string($this->keySet)) { 170 | $this->keySet = $this->formatJwksForCache($this->keySet); 171 | } 172 | } 173 | } 174 | 175 | if (!isset($this->keySet[$keyId])) { 176 | if ($this->rateLimitExceeded()) { 177 | return false; 178 | } 179 | $request = $this->httpFactory->createRequest('GET', $this->jwksUri); 180 | $jwksResponse = $this->httpClient->sendRequest($request); 181 | if ($jwksResponse->getStatusCode() !== 200) { 182 | throw new UnexpectedValueException( 183 | \sprintf('HTTP Error: %d %s for URI "%s"', 184 | $jwksResponse->getStatusCode(), 185 | $jwksResponse->getReasonPhrase(), 186 | $this->jwksUri, 187 | ), 188 | $jwksResponse->getStatusCode() 189 | ); 190 | } 191 | $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); 192 | 193 | if (!isset($this->keySet[$keyId])) { 194 | return false; 195 | } 196 | 197 | $item = $this->getCacheItem(); 198 | $item->set($this->keySet); 199 | if ($this->expiresAfter) { 200 | $item->expiresAfter($this->expiresAfter); 201 | } 202 | $this->cache->save($item); 203 | } 204 | 205 | return true; 206 | } 207 | 208 | private function rateLimitExceeded(): bool 209 | { 210 | if (!$this->rateLimit) { 211 | return false; 212 | } 213 | 214 | $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); 215 | 216 | $cacheItemData = []; 217 | if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) { 218 | $cacheItemData = $data; 219 | } 220 | 221 | $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0; 222 | $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC')); 223 | 224 | if (++$callsPerMinute > $this->maxCallsPerMinute) { 225 | return true; 226 | } 227 | 228 | $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]); 229 | $cacheItem->expiresAt($expiry); 230 | $this->cache->save($cacheItem); 231 | return false; 232 | } 233 | 234 | private function getCacheItem(): CacheItemInterface 235 | { 236 | if (\is_null($this->cacheItem)) { 237 | $this->cacheItem = $this->cache->getItem($this->cacheKey); 238 | } 239 | 240 | return $this->cacheItem; 241 | } 242 | 243 | private function setCacheKeys(): void 244 | { 245 | if (empty($this->jwksUri)) { 246 | throw new RuntimeException('JWKS URI is empty'); 247 | } 248 | 249 | // ensure we do not have illegal characters 250 | $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); 251 | 252 | // add prefix 253 | $key = $this->cacheKeyPrefix . $key; 254 | 255 | // Hash keys if they exceed $maxKeyLength of 64 256 | if (\strlen($key) > $this->maxKeyLength) { 257 | $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); 258 | } 259 | 260 | $this->cacheKey = $key; 261 | 262 | if ($this->rateLimit) { 263 | // add prefix 264 | $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; 265 | 266 | // Hash keys if they exceed $maxKeyLength of 64 267 | if (\strlen($rateLimitKey) > $this->maxKeyLength) { 268 | $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); 269 | } 270 | 271 | $this->rateLimitCacheKey = $rateLimitKey; 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/ExpiredException.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 12 | } 13 | 14 | public function getPayload(): object 15 | { 16 | return $this->payload; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/JWK.php: -------------------------------------------------------------------------------- 1 | 18 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 19 | * @link https://github.com/firebase/php-jwt 20 | */ 21 | class JWK 22 | { 23 | private const OID = '1.2.840.10045.2.1'; 24 | private const ASN1_OBJECT_IDENTIFIER = 0x06; 25 | private const ASN1_SEQUENCE = 0x10; // also defined in JWT 26 | private const ASN1_BIT_STRING = 0x03; 27 | private const EC_CURVES = [ 28 | 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 29 | 'secp256k1' => '1.3.132.0.10', // Len: 64 30 | 'P-384' => '1.3.132.0.34', // Len: 96 31 | // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) 32 | ]; 33 | 34 | // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. 35 | // This library supports the following subtypes: 36 | private const OKP_SUBTYPES = [ 37 | 'Ed25519' => true, // RFC 8037 38 | ]; 39 | 40 | /** 41 | * Parse a set of JWK keys 42 | * 43 | * @param array $jwks The JSON Web Key Set as an associative array 44 | * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the 45 | * JSON Web Key Set 46 | * 47 | * @return array An associative array of key IDs (kid) to Key objects 48 | * 49 | * @throws InvalidArgumentException Provided JWK Set is empty 50 | * @throws UnexpectedValueException Provided JWK Set was invalid 51 | * @throws DomainException OpenSSL failure 52 | * 53 | * @uses parseKey 54 | */ 55 | public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array 56 | { 57 | $keys = []; 58 | 59 | if (!isset($jwks['keys'])) { 60 | throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); 61 | } 62 | 63 | if (empty($jwks['keys'])) { 64 | throw new InvalidArgumentException('JWK Set did not contain any keys'); 65 | } 66 | 67 | foreach ($jwks['keys'] as $k => $v) { 68 | $kid = isset($v['kid']) ? $v['kid'] : $k; 69 | if ($key = self::parseKey($v, $defaultAlg)) { 70 | $keys[(string) $kid] = $key; 71 | } 72 | } 73 | 74 | if (0 === \count($keys)) { 75 | throw new UnexpectedValueException('No supported algorithms found in JWK Set'); 76 | } 77 | 78 | return $keys; 79 | } 80 | 81 | /** 82 | * Parse a JWK key 83 | * 84 | * @param array $jwk An individual JWK 85 | * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the 86 | * JSON Web Key Set 87 | * 88 | * @return Key The key object for the JWK 89 | * 90 | * @throws InvalidArgumentException Provided JWK is empty 91 | * @throws UnexpectedValueException Provided JWK was invalid 92 | * @throws DomainException OpenSSL failure 93 | * 94 | * @uses createPemFromModulusAndExponent 95 | */ 96 | public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key 97 | { 98 | if (empty($jwk)) { 99 | throw new InvalidArgumentException('JWK must not be empty'); 100 | } 101 | 102 | if (!isset($jwk['kty'])) { 103 | throw new UnexpectedValueException('JWK must contain a "kty" parameter'); 104 | } 105 | 106 | if (!isset($jwk['alg'])) { 107 | if (\is_null($defaultAlg)) { 108 | // The "alg" parameter is optional in a KTY, but an algorithm is required 109 | // for parsing in this library. Use the $defaultAlg parameter when parsing the 110 | // key set in order to prevent this error. 111 | // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 112 | throw new UnexpectedValueException('JWK must contain an "alg" parameter'); 113 | } 114 | $jwk['alg'] = $defaultAlg; 115 | } 116 | 117 | switch ($jwk['kty']) { 118 | case 'RSA': 119 | if (!empty($jwk['d'])) { 120 | throw new UnexpectedValueException('RSA private keys are not supported'); 121 | } 122 | if (!isset($jwk['n']) || !isset($jwk['e'])) { 123 | throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); 124 | } 125 | 126 | $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); 127 | $publicKey = \openssl_pkey_get_public($pem); 128 | if (false === $publicKey) { 129 | throw new DomainException( 130 | 'OpenSSL error: ' . \openssl_error_string() 131 | ); 132 | } 133 | return new Key($publicKey, $jwk['alg']); 134 | case 'EC': 135 | if (isset($jwk['d'])) { 136 | // The key is actually a private key 137 | throw new UnexpectedValueException('Key data must be for a public key'); 138 | } 139 | 140 | if (empty($jwk['crv'])) { 141 | throw new UnexpectedValueException('crv not set'); 142 | } 143 | 144 | if (!isset(self::EC_CURVES[$jwk['crv']])) { 145 | throw new DomainException('Unrecognised or unsupported EC curve'); 146 | } 147 | 148 | if (empty($jwk['x']) || empty($jwk['y'])) { 149 | throw new UnexpectedValueException('x and y not set'); 150 | } 151 | 152 | $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); 153 | return new Key($publicKey, $jwk['alg']); 154 | case 'OKP': 155 | if (isset($jwk['d'])) { 156 | // The key is actually a private key 157 | throw new UnexpectedValueException('Key data must be for a public key'); 158 | } 159 | 160 | if (!isset($jwk['crv'])) { 161 | throw new UnexpectedValueException('crv not set'); 162 | } 163 | 164 | if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { 165 | throw new DomainException('Unrecognised or unsupported OKP key subtype'); 166 | } 167 | 168 | if (empty($jwk['x'])) { 169 | throw new UnexpectedValueException('x not set'); 170 | } 171 | 172 | // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. 173 | $publicKey = JWT::convertBase64urlToBase64($jwk['x']); 174 | return new Key($publicKey, $jwk['alg']); 175 | case 'oct': 176 | if (!isset($jwk['k'])) { 177 | throw new UnexpectedValueException('k not set'); 178 | } 179 | 180 | return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']); 181 | default: 182 | break; 183 | } 184 | 185 | return null; 186 | } 187 | 188 | /** 189 | * Converts the EC JWK values to pem format. 190 | * 191 | * @param string $crv The EC curve (only P-256 & P-384 is supported) 192 | * @param string $x The EC x-coordinate 193 | * @param string $y The EC y-coordinate 194 | * 195 | * @return string 196 | */ 197 | private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string 198 | { 199 | $pem = 200 | self::encodeDER( 201 | self::ASN1_SEQUENCE, 202 | self::encodeDER( 203 | self::ASN1_SEQUENCE, 204 | self::encodeDER( 205 | self::ASN1_OBJECT_IDENTIFIER, 206 | self::encodeOID(self::OID) 207 | ) 208 | . self::encodeDER( 209 | self::ASN1_OBJECT_IDENTIFIER, 210 | self::encodeOID(self::EC_CURVES[$crv]) 211 | ) 212 | ) . 213 | self::encodeDER( 214 | self::ASN1_BIT_STRING, 215 | \chr(0x00) . \chr(0x04) 216 | . JWT::urlsafeB64Decode($x) 217 | . JWT::urlsafeB64Decode($y) 218 | ) 219 | ); 220 | 221 | return \sprintf( 222 | "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", 223 | wordwrap(base64_encode($pem), 64, "\n", true) 224 | ); 225 | } 226 | 227 | /** 228 | * Create a public key represented in PEM format from RSA modulus and exponent information 229 | * 230 | * @param string $n The RSA modulus encoded in Base64 231 | * @param string $e The RSA exponent encoded in Base64 232 | * 233 | * @return string The RSA public key represented in PEM format 234 | * 235 | * @uses encodeLength 236 | */ 237 | private static function createPemFromModulusAndExponent( 238 | string $n, 239 | string $e 240 | ): string { 241 | $mod = JWT::urlsafeB64Decode($n); 242 | $exp = JWT::urlsafeB64Decode($e); 243 | 244 | $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); 245 | $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); 246 | 247 | $rsaPublicKey = \pack( 248 | 'Ca*a*a*', 249 | 48, 250 | self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), 251 | $modulus, 252 | $publicExponent 253 | ); 254 | 255 | // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. 256 | $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA 257 | $rsaPublicKey = \chr(0) . $rsaPublicKey; 258 | $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; 259 | 260 | $rsaPublicKey = \pack( 261 | 'Ca*a*', 262 | 48, 263 | self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), 264 | $rsaOID . $rsaPublicKey 265 | ); 266 | 267 | return "-----BEGIN PUBLIC KEY-----\r\n" . 268 | \chunk_split(\base64_encode($rsaPublicKey), 64) . 269 | '-----END PUBLIC KEY-----'; 270 | } 271 | 272 | /** 273 | * DER-encode the length 274 | * 275 | * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See 276 | * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. 277 | * 278 | * @param int $length 279 | * @return string 280 | */ 281 | private static function encodeLength(int $length): string 282 | { 283 | if ($length <= 0x7F) { 284 | return \chr($length); 285 | } 286 | 287 | $temp = \ltrim(\pack('N', $length), \chr(0)); 288 | 289 | return \pack('Ca*', 0x80 | \strlen($temp), $temp); 290 | } 291 | 292 | /** 293 | * Encodes a value into a DER object. 294 | * Also defined in Firebase\JWT\JWT 295 | * 296 | * @param int $type DER tag 297 | * @param string $value the value to encode 298 | * @return string the encoded object 299 | */ 300 | private static function encodeDER(int $type, string $value): string 301 | { 302 | $tag_header = 0; 303 | if ($type === self::ASN1_SEQUENCE) { 304 | $tag_header |= 0x20; 305 | } 306 | 307 | // Type 308 | $der = \chr($tag_header | $type); 309 | 310 | // Length 311 | $der .= \chr(\strlen($value)); 312 | 313 | return $der . $value; 314 | } 315 | 316 | /** 317 | * Encodes a string into a DER-encoded OID. 318 | * 319 | * @param string $oid the OID string 320 | * @return string the binary DER-encoded OID 321 | */ 322 | private static function encodeOID(string $oid): string 323 | { 324 | $octets = explode('.', $oid); 325 | 326 | // Get the first octet 327 | $first = (int) array_shift($octets); 328 | $second = (int) array_shift($octets); 329 | $oid = \chr($first * 40 + $second); 330 | 331 | // Iterate over subsequent octets 332 | foreach ($octets as $octet) { 333 | if ($octet == 0) { 334 | $oid .= \chr(0x00); 335 | continue; 336 | } 337 | $bin = ''; 338 | 339 | while ($octet) { 340 | $bin .= \chr(0x80 | ($octet & 0x7f)); 341 | $octet >>= 7; 342 | } 343 | $bin[0] = $bin[0] & \chr(0x7f); 344 | 345 | // Convert to big endian if necessary 346 | if (pack('V', 65534) == pack('L', 65534)) { 347 | $oid .= strrev($bin); 348 | } else { 349 | $oid .= $bin; 350 | } 351 | } 352 | 353 | return $oid; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/JWT.php: -------------------------------------------------------------------------------- 1 | 24 | * @author Anant Narayanan 25 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 26 | * @link https://github.com/firebase/php-jwt 27 | */ 28 | class JWT 29 | { 30 | private const ASN1_INTEGER = 0x02; 31 | private const ASN1_SEQUENCE = 0x10; 32 | private const ASN1_BIT_STRING = 0x03; 33 | 34 | /** 35 | * When checking nbf, iat or expiration times, 36 | * we want to provide some extra leeway time to 37 | * account for clock skew. 38 | * 39 | * @var int 40 | */ 41 | public static $leeway = 0; 42 | 43 | /** 44 | * Allow the current timestamp to be specified. 45 | * Useful for fixing a value within unit testing. 46 | * Will default to PHP time() value if null. 47 | * 48 | * @var ?int 49 | */ 50 | public static $timestamp = null; 51 | 52 | /** 53 | * @var array 54 | */ 55 | public static $supported_algs = [ 56 | 'ES384' => ['openssl', 'SHA384'], 57 | 'ES256' => ['openssl', 'SHA256'], 58 | 'ES256K' => ['openssl', 'SHA256'], 59 | 'HS256' => ['hash_hmac', 'SHA256'], 60 | 'HS384' => ['hash_hmac', 'SHA384'], 61 | 'HS512' => ['hash_hmac', 'SHA512'], 62 | 'RS256' => ['openssl', 'SHA256'], 63 | 'RS384' => ['openssl', 'SHA384'], 64 | 'RS512' => ['openssl', 'SHA512'], 65 | 'EdDSA' => ['sodium_crypto', 'EdDSA'], 66 | ]; 67 | 68 | /** 69 | * Decodes a JWT string into a PHP object. 70 | * 71 | * @param string $jwt The JWT 72 | * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs 73 | * (kid) to Key objects. 74 | * If the algorithm used is asymmetric, this is 75 | * the public key. 76 | * Each Key object contains an algorithm and 77 | * matching key. 78 | * Supported algorithms are 'ES384','ES256', 79 | * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' 80 | * and 'RS512'. 81 | * @param stdClass $headers Optional. Populates stdClass with headers. 82 | * 83 | * @return stdClass The JWT's payload as a PHP object 84 | * 85 | * @throws InvalidArgumentException Provided key/key-array was empty or malformed 86 | * @throws DomainException Provided JWT is malformed 87 | * @throws UnexpectedValueException Provided JWT was invalid 88 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 89 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 90 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 91 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 92 | * 93 | * @uses jsonDecode 94 | * @uses urlsafeB64Decode 95 | */ 96 | public static function decode( 97 | string $jwt, 98 | $keyOrKeyArray, 99 | ?stdClass &$headers = null 100 | ): stdClass { 101 | // Validate JWT 102 | $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; 103 | 104 | if (empty($keyOrKeyArray)) { 105 | throw new InvalidArgumentException('Key may not be empty'); 106 | } 107 | $tks = \explode('.', $jwt); 108 | if (\count($tks) !== 3) { 109 | throw new UnexpectedValueException('Wrong number of segments'); 110 | } 111 | list($headb64, $bodyb64, $cryptob64) = $tks; 112 | $headerRaw = static::urlsafeB64Decode($headb64); 113 | if (null === ($header = static::jsonDecode($headerRaw))) { 114 | throw new UnexpectedValueException('Invalid header encoding'); 115 | } 116 | if ($headers !== null) { 117 | $headers = $header; 118 | } 119 | $payloadRaw = static::urlsafeB64Decode($bodyb64); 120 | if (null === ($payload = static::jsonDecode($payloadRaw))) { 121 | throw new UnexpectedValueException('Invalid claims encoding'); 122 | } 123 | if (\is_array($payload)) { 124 | // prevent PHP Fatal Error in edge-cases when payload is empty array 125 | $payload = (object) $payload; 126 | } 127 | if (!$payload instanceof stdClass) { 128 | throw new UnexpectedValueException('Payload must be a JSON object'); 129 | } 130 | if (isset($payload->iat) && !\is_numeric($payload->iat)) { 131 | throw new UnexpectedValueException('Payload iat must be a number'); 132 | } 133 | if (isset($payload->nbf) && !\is_numeric($payload->nbf)) { 134 | throw new UnexpectedValueException('Payload nbf must be a number'); 135 | } 136 | if (isset($payload->exp) && !\is_numeric($payload->exp)) { 137 | throw new UnexpectedValueException('Payload exp must be a number'); 138 | } 139 | 140 | $sig = static::urlsafeB64Decode($cryptob64); 141 | if (empty($header->alg)) { 142 | throw new UnexpectedValueException('Empty algorithm'); 143 | } 144 | if (empty(static::$supported_algs[$header->alg])) { 145 | throw new UnexpectedValueException('Algorithm not supported'); 146 | } 147 | 148 | $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); 149 | 150 | // Check the algorithm 151 | if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { 152 | // See issue #351 153 | throw new UnexpectedValueException('Incorrect key for this algorithm'); 154 | } 155 | if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { 156 | // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures 157 | $sig = self::signatureToDER($sig); 158 | } 159 | if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { 160 | throw new SignatureInvalidException('Signature verification failed'); 161 | } 162 | 163 | // Check the nbf if it is defined. This is the time that the 164 | // token can actually be used. If it's not yet that time, abort. 165 | if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { 166 | $ex = new BeforeValidException( 167 | 'Cannot handle token with nbf prior to ' . \date(DateTime::ATOM, (int) floor($payload->nbf)) 168 | ); 169 | $ex->setPayload($payload); 170 | throw $ex; 171 | } 172 | 173 | // Check that this token has been created before 'now'. This prevents 174 | // using tokens that have been created for later use (and haven't 175 | // correctly used the nbf claim). 176 | if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { 177 | $ex = new BeforeValidException( 178 | 'Cannot handle token with iat prior to ' . \date(DateTime::ATOM, (int) floor($payload->iat)) 179 | ); 180 | $ex->setPayload($payload); 181 | throw $ex; 182 | } 183 | 184 | // Check if this token has expired. 185 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { 186 | $ex = new ExpiredException('Expired token'); 187 | $ex->setPayload($payload); 188 | throw $ex; 189 | } 190 | 191 | return $payload; 192 | } 193 | 194 | /** 195 | * Converts and signs a PHP array into a JWT string. 196 | * 197 | * @param array $payload PHP array 198 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. 199 | * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', 200 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 201 | * @param string $keyId 202 | * @param array $head An array with header elements to attach 203 | * 204 | * @return string A signed JWT 205 | * 206 | * @uses jsonEncode 207 | * @uses urlsafeB64Encode 208 | */ 209 | public static function encode( 210 | array $payload, 211 | $key, 212 | string $alg, 213 | ?string $keyId = null, 214 | ?array $head = null 215 | ): string { 216 | $header = ['typ' => 'JWT']; 217 | if (isset($head)) { 218 | $header = \array_merge($header, $head); 219 | } 220 | $header['alg'] = $alg; 221 | if ($keyId !== null) { 222 | $header['kid'] = $keyId; 223 | } 224 | $segments = []; 225 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); 226 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); 227 | $signing_input = \implode('.', $segments); 228 | 229 | $signature = static::sign($signing_input, $key, $alg); 230 | $segments[] = static::urlsafeB64Encode($signature); 231 | 232 | return \implode('.', $segments); 233 | } 234 | 235 | /** 236 | * Sign a string with a given key and algorithm. 237 | * 238 | * @param string $msg The message to sign 239 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. 240 | * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', 241 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 242 | * 243 | * @return string An encrypted message 244 | * 245 | * @throws DomainException Unsupported algorithm or bad key was specified 246 | */ 247 | public static function sign( 248 | string $msg, 249 | $key, 250 | string $alg 251 | ): string { 252 | if (empty(static::$supported_algs[$alg])) { 253 | throw new DomainException('Algorithm not supported'); 254 | } 255 | list($function, $algorithm) = static::$supported_algs[$alg]; 256 | switch ($function) { 257 | case 'hash_hmac': 258 | if (!\is_string($key)) { 259 | throw new InvalidArgumentException('key must be a string when using hmac'); 260 | } 261 | return \hash_hmac($algorithm, $msg, $key, true); 262 | case 'openssl': 263 | $signature = ''; 264 | if (!\is_resource($key) && !openssl_pkey_get_private($key)) { 265 | throw new DomainException('OpenSSL unable to validate key'); 266 | } 267 | $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line 268 | if (!$success) { 269 | throw new DomainException('OpenSSL unable to sign data'); 270 | } 271 | if ($alg === 'ES256' || $alg === 'ES256K') { 272 | $signature = self::signatureFromDER($signature, 256); 273 | } elseif ($alg === 'ES384') { 274 | $signature = self::signatureFromDER($signature, 384); 275 | } 276 | return $signature; 277 | case 'sodium_crypto': 278 | if (!\function_exists('sodium_crypto_sign_detached')) { 279 | throw new DomainException('libsodium is not available'); 280 | } 281 | if (!\is_string($key)) { 282 | throw new InvalidArgumentException('key must be a string when using EdDSA'); 283 | } 284 | try { 285 | // The last non-empty line is used as the key. 286 | $lines = array_filter(explode("\n", $key)); 287 | $key = base64_decode((string) end($lines)); 288 | if (\strlen($key) === 0) { 289 | throw new DomainException('Key cannot be empty string'); 290 | } 291 | return sodium_crypto_sign_detached($msg, $key); 292 | } catch (Exception $e) { 293 | throw new DomainException($e->getMessage(), 0, $e); 294 | } 295 | } 296 | 297 | throw new DomainException('Algorithm not supported'); 298 | } 299 | 300 | /** 301 | * Verify a signature with the message, key and method. Not all methods 302 | * are symmetric, so we must have a separate verify and sign method. 303 | * 304 | * @param string $msg The original message (header and body) 305 | * @param string $signature The original signature 306 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey 307 | * @param string $alg The algorithm 308 | * 309 | * @return bool 310 | * 311 | * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure 312 | */ 313 | private static function verify( 314 | string $msg, 315 | string $signature, 316 | $keyMaterial, 317 | string $alg 318 | ): bool { 319 | if (empty(static::$supported_algs[$alg])) { 320 | throw new DomainException('Algorithm not supported'); 321 | } 322 | 323 | list($function, $algorithm) = static::$supported_algs[$alg]; 324 | switch ($function) { 325 | case 'openssl': 326 | $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line 327 | if ($success === 1) { 328 | return true; 329 | } 330 | if ($success === 0) { 331 | return false; 332 | } 333 | // returns 1 on success, 0 on failure, -1 on error. 334 | throw new DomainException( 335 | 'OpenSSL error: ' . \openssl_error_string() 336 | ); 337 | case 'sodium_crypto': 338 | if (!\function_exists('sodium_crypto_sign_verify_detached')) { 339 | throw new DomainException('libsodium is not available'); 340 | } 341 | if (!\is_string($keyMaterial)) { 342 | throw new InvalidArgumentException('key must be a string when using EdDSA'); 343 | } 344 | try { 345 | // The last non-empty line is used as the key. 346 | $lines = array_filter(explode("\n", $keyMaterial)); 347 | $key = base64_decode((string) end($lines)); 348 | if (\strlen($key) === 0) { 349 | throw new DomainException('Key cannot be empty string'); 350 | } 351 | if (\strlen($signature) === 0) { 352 | throw new DomainException('Signature cannot be empty string'); 353 | } 354 | return sodium_crypto_sign_verify_detached($signature, $msg, $key); 355 | } catch (Exception $e) { 356 | throw new DomainException($e->getMessage(), 0, $e); 357 | } 358 | case 'hash_hmac': 359 | default: 360 | if (!\is_string($keyMaterial)) { 361 | throw new InvalidArgumentException('key must be a string when using hmac'); 362 | } 363 | $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); 364 | return self::constantTimeEquals($hash, $signature); 365 | } 366 | } 367 | 368 | /** 369 | * Decode a JSON string into a PHP object. 370 | * 371 | * @param string $input JSON string 372 | * 373 | * @return mixed The decoded JSON string 374 | * 375 | * @throws DomainException Provided string was invalid JSON 376 | */ 377 | public static function jsonDecode(string $input) 378 | { 379 | $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); 380 | 381 | if ($errno = \json_last_error()) { 382 | self::handleJsonError($errno); 383 | } elseif ($obj === null && $input !== 'null') { 384 | throw new DomainException('Null result with non-null input'); 385 | } 386 | return $obj; 387 | } 388 | 389 | /** 390 | * Encode a PHP array into a JSON string. 391 | * 392 | * @param array $input A PHP array 393 | * 394 | * @return string JSON representation of the PHP array 395 | * 396 | * @throws DomainException Provided object could not be encoded to valid JSON 397 | */ 398 | public static function jsonEncode(array $input): string 399 | { 400 | $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); 401 | if ($errno = \json_last_error()) { 402 | self::handleJsonError($errno); 403 | } elseif ($json === 'null') { 404 | throw new DomainException('Null result with non-null input'); 405 | } 406 | if ($json === false) { 407 | throw new DomainException('Provided object could not be encoded to valid JSON'); 408 | } 409 | return $json; 410 | } 411 | 412 | /** 413 | * Decode a string with URL-safe Base64. 414 | * 415 | * @param string $input A Base64 encoded string 416 | * 417 | * @return string A decoded string 418 | * 419 | * @throws InvalidArgumentException invalid base64 characters 420 | */ 421 | public static function urlsafeB64Decode(string $input): string 422 | { 423 | return \base64_decode(self::convertBase64UrlToBase64($input)); 424 | } 425 | 426 | /** 427 | * Convert a string in the base64url (URL-safe Base64) encoding to standard base64. 428 | * 429 | * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) 430 | * 431 | * @return string A Base64 encoded string with standard characters (+/) and padding (=), when 432 | * needed. 433 | * 434 | * @see https://www.rfc-editor.org/rfc/rfc4648 435 | */ 436 | public static function convertBase64UrlToBase64(string $input): string 437 | { 438 | $remainder = \strlen($input) % 4; 439 | if ($remainder) { 440 | $padlen = 4 - $remainder; 441 | $input .= \str_repeat('=', $padlen); 442 | } 443 | return \strtr($input, '-_', '+/'); 444 | } 445 | 446 | /** 447 | * Encode a string with URL-safe Base64. 448 | * 449 | * @param string $input The string you want encoded 450 | * 451 | * @return string The base64 encode of what you passed in 452 | */ 453 | public static function urlsafeB64Encode(string $input): string 454 | { 455 | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); 456 | } 457 | 458 | 459 | /** 460 | * Determine if an algorithm has been provided for each Key 461 | * 462 | * @param Key|ArrayAccess|array $keyOrKeyArray 463 | * @param string|null $kid 464 | * 465 | * @throws UnexpectedValueException 466 | * 467 | * @return Key 468 | */ 469 | private static function getKey( 470 | $keyOrKeyArray, 471 | ?string $kid 472 | ): Key { 473 | if ($keyOrKeyArray instanceof Key) { 474 | return $keyOrKeyArray; 475 | } 476 | 477 | if (empty($kid) && $kid !== '0') { 478 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); 479 | } 480 | 481 | if ($keyOrKeyArray instanceof CachedKeySet) { 482 | // Skip "isset" check, as this will automatically refresh if not set 483 | return $keyOrKeyArray[$kid]; 484 | } 485 | 486 | if (!isset($keyOrKeyArray[$kid])) { 487 | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); 488 | } 489 | 490 | return $keyOrKeyArray[$kid]; 491 | } 492 | 493 | /** 494 | * @param string $left The string of known length to compare against 495 | * @param string $right The user-supplied string 496 | * @return bool 497 | */ 498 | public static function constantTimeEquals(string $left, string $right): bool 499 | { 500 | if (\function_exists('hash_equals')) { 501 | return \hash_equals($left, $right); 502 | } 503 | $len = \min(self::safeStrlen($left), self::safeStrlen($right)); 504 | 505 | $status = 0; 506 | for ($i = 0; $i < $len; $i++) { 507 | $status |= (\ord($left[$i]) ^ \ord($right[$i])); 508 | } 509 | $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); 510 | 511 | return ($status === 0); 512 | } 513 | 514 | /** 515 | * Helper method to create a JSON error. 516 | * 517 | * @param int $errno An error number from json_last_error() 518 | * 519 | * @throws DomainException 520 | * 521 | * @return void 522 | */ 523 | private static function handleJsonError(int $errno): void 524 | { 525 | $messages = [ 526 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 527 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', 528 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 529 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 530 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 531 | ]; 532 | throw new DomainException( 533 | isset($messages[$errno]) 534 | ? $messages[$errno] 535 | : 'Unknown JSON error: ' . $errno 536 | ); 537 | } 538 | 539 | /** 540 | * Get the number of bytes in cryptographic strings. 541 | * 542 | * @param string $str 543 | * 544 | * @return int 545 | */ 546 | private static function safeStrlen(string $str): int 547 | { 548 | if (\function_exists('mb_strlen')) { 549 | return \mb_strlen($str, '8bit'); 550 | } 551 | return \strlen($str); 552 | } 553 | 554 | /** 555 | * Convert an ECDSA signature to an ASN.1 DER sequence 556 | * 557 | * @param string $sig The ECDSA signature to convert 558 | * @return string The encoded DER object 559 | */ 560 | private static function signatureToDER(string $sig): string 561 | { 562 | // Separate the signature into r-value and s-value 563 | $length = max(1, (int) (\strlen($sig) / 2)); 564 | list($r, $s) = \str_split($sig, $length); 565 | 566 | // Trim leading zeros 567 | $r = \ltrim($r, "\x00"); 568 | $s = \ltrim($s, "\x00"); 569 | 570 | // Convert r-value and s-value from unsigned big-endian integers to 571 | // signed two's complement 572 | if (\ord($r[0]) > 0x7f) { 573 | $r = "\x00" . $r; 574 | } 575 | if (\ord($s[0]) > 0x7f) { 576 | $s = "\x00" . $s; 577 | } 578 | 579 | return self::encodeDER( 580 | self::ASN1_SEQUENCE, 581 | self::encodeDER(self::ASN1_INTEGER, $r) . 582 | self::encodeDER(self::ASN1_INTEGER, $s) 583 | ); 584 | } 585 | 586 | /** 587 | * Encodes a value into a DER object. 588 | * 589 | * @param int $type DER tag 590 | * @param string $value the value to encode 591 | * 592 | * @return string the encoded object 593 | */ 594 | private static function encodeDER(int $type, string $value): string 595 | { 596 | $tag_header = 0; 597 | if ($type === self::ASN1_SEQUENCE) { 598 | $tag_header |= 0x20; 599 | } 600 | 601 | // Type 602 | $der = \chr($tag_header | $type); 603 | 604 | // Length 605 | $der .= \chr(\strlen($value)); 606 | 607 | return $der . $value; 608 | } 609 | 610 | /** 611 | * Encodes signature from a DER object. 612 | * 613 | * @param string $der binary signature in DER format 614 | * @param int $keySize the number of bits in the key 615 | * 616 | * @return string the signature 617 | */ 618 | private static function signatureFromDER(string $der, int $keySize): string 619 | { 620 | // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE 621 | list($offset, $_) = self::readDER($der); 622 | list($offset, $r) = self::readDER($der, $offset); 623 | list($offset, $s) = self::readDER($der, $offset); 624 | 625 | // Convert r-value and s-value from signed two's compliment to unsigned 626 | // big-endian integers 627 | $r = \ltrim($r, "\x00"); 628 | $s = \ltrim($s, "\x00"); 629 | 630 | // Pad out r and s so that they are $keySize bits long 631 | $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); 632 | $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); 633 | 634 | return $r . $s; 635 | } 636 | 637 | /** 638 | * Reads binary DER-encoded data and decodes into a single object 639 | * 640 | * @param string $der the binary data in DER format 641 | * @param int $offset the offset of the data stream containing the object 642 | * to decode 643 | * 644 | * @return array{int, string|null} the new offset and the decoded object 645 | */ 646 | private static function readDER(string $der, int $offset = 0): array 647 | { 648 | $pos = $offset; 649 | $size = \strlen($der); 650 | $constructed = (\ord($der[$pos]) >> 5) & 0x01; 651 | $type = \ord($der[$pos++]) & 0x1f; 652 | 653 | // Length 654 | $len = \ord($der[$pos++]); 655 | if ($len & 0x80) { 656 | $n = $len & 0x1f; 657 | $len = 0; 658 | while ($n-- && $pos < $size) { 659 | $len = ($len << 8) | \ord($der[$pos++]); 660 | } 661 | } 662 | 663 | // Value 664 | if ($type === self::ASN1_BIT_STRING) { 665 | $pos++; // Skip the first contents octet (padding indicator) 666 | $data = \substr($der, $pos, $len - 1); 667 | $pos += $len - 1; 668 | } elseif (!$constructed) { 669 | $data = \substr($der, $pos, $len); 670 | $pos += $len; 671 | } else { 672 | $data = null; 673 | } 674 | 675 | return [$pos, $data]; 676 | } 677 | } 678 | -------------------------------------------------------------------------------- /src/JWTExceptionWithPayloadInterface.php: -------------------------------------------------------------------------------- 1 | algorithm; 46 | } 47 | 48 | /** 49 | * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate 50 | */ 51 | public function getKeyMaterial() 52 | { 53 | return $this->keyMaterial; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/SignatureInvalidException.php: -------------------------------------------------------------------------------- 1 |