├── .gitignore ├── .travis.yml ├── CREDITS ├── LICENSE ├── README.md ├── config.m4 ├── config.w32 ├── example ├── ecdsa.php ├── hmac.php ├── none.php └── rsa.php ├── jwt.c ├── jwt.php ├── openssl.c ├── package.xml ├── php_jwt.h ├── tests ├── 001.phpt ├── 002.phpt ├── 003.phpt ├── 004.phpt ├── 005.phpt ├── 006.phpt ├── 007.phpt ├── 008.phpt ├── 009.phpt ├── 010.phpt ├── 011.phpt ├── 012.phpt ├── 013.phpt ├── 014.phpt ├── 015.phpt └── 016.phpt └── travis ├── compile.sh └── run-test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | .deps 54 | *.lo 55 | *.la 56 | .libs 57 | acinclude.m4 58 | aclocal.m4 59 | autom4te.cache 60 | build 61 | configure.ac 62 | config.guess 63 | config.h 64 | config.h.in 65 | config.log 66 | config.nice 67 | config.status 68 | config.sub 69 | configure 70 | configure.in 71 | include 72 | install-sh 73 | libtool 74 | ltmain.sh 75 | Makefile 76 | Makefile.fragments 77 | Makefile.global 78 | Makefile.objects 79 | missing 80 | mkinstalldirs 81 | modules 82 | run-tests.php 83 | tests/*/*.diff 84 | tests/*/*.out 85 | tests/*/*.php 86 | tests/*/*.exp 87 | tests/*/*.log 88 | tests/*/*.sh 89 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | - 7.4 9 | 10 | notifications: 11 | email: false 12 | 13 | sudo: enabled 14 | 15 | #Compile 16 | before_script: 17 | - openssl version -a 18 | - ./travis/compile.sh 19 | 20 | # Run PHPs run-tests.php 21 | script: 22 | - ./travis/run-test.sh 23 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | JWT 2 | ZiHang Gao -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------- 2 | The PHP License, version 3.01 3 | Copyright (c) 1999 - 2011 The PHP Group. All rights reserved. 4 | -------------------------------------------------------------------- 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, is permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | 3. The name "PHP" must not be used to endorse or promote products 19 | derived from this software without prior written permission. For 20 | written permission, please contact group@php.net. 21 | 22 | 4. Products derived from this software may not be called "PHP", nor 23 | may "PHP" appear in their name, without prior written permission 24 | from group@php.net. You may indicate that your software works in 25 | conjunction with PHP by saying "Foo for PHP" instead of calling 26 | it "PHP Foo" or "phpfoo" 27 | 28 | 5. The PHP Group may publish revised and/or new versions of the 29 | license from time to time. Each version will be given a 30 | distinguishing version number. 31 | Once covered code has been published under a particular version 32 | of the license, you may always continue to use it under the terms 33 | of that version. You may also choose to use such covered code 34 | under the terms of any subsequent version of the license 35 | published by the PHP Group. No one other than the PHP Group has 36 | the right to modify the terms applicable to covered code created 37 | under this License. 38 | 39 | 6. Redistributions of any form whatsoever must retain the following 40 | acknowledgment: 41 | "This product includes PHP software, freely available from 42 | ". 43 | 44 | THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND 45 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 46 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 47 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP 48 | DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 49 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 51 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 52 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 53 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 54 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 55 | OF THE POSSIBILITY OF SUCH DAMAGE. 56 | 57 | -------------------------------------------------------------------- 58 | 59 | This software consists of voluntary contributions made by many 60 | individuals on behalf of the PHP Group. 61 | 62 | The PHP Group can be contacted via Email at group@php.net. 63 | 64 | For more information on the PHP Group and the PHP project, 65 | please see . 66 | 67 | PHP includes the Zend Engine, freely available at 68 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-image]][travis-url] 2 | ![PHP](https://img.shields.io/badge/PHP-%3E%3D7.0.0-orange.svg) 3 | ![OpenSSL](https://img.shields.io/badge/OpenSSL-%3E%3D1.0.1f-orange.svg) 4 | ![branch](https://img.shields.io/badge/branch-master-brightgreen.svg) 5 | ![license](https://img.shields.io/badge/License-PHP/3.01-blue.svg) 6 | 7 | > A PHP extension for [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) 8 | 9 | ## Requirement 10 | 11 | - PHP 7 + 12 | - PHP json extension, need to json extension before loading JWT extension. 13 | - OpenSSL (Version >= 1.1.0j) Might work with older version as well, but I did not check that. 14 | 15 | ## Install 16 | 17 | ```shell 18 | $ git clone https://github.com/cdoco/php-jwt.git 19 | $ cd php-jwt 20 | $ phpize && ./configure --with-openssl=/path/to/openssl 21 | $ make && make install 22 | ``` 23 | 24 | ## Quick [Example](https://github.com/cdoco/php-jwt/tree/master/example) 25 | 26 | ```php 27 | $key = "example-hmac-key"; 28 | $payload = [ 29 | "data" => [ 30 | "name" => "ZiHang Gao", 31 | "admin" => true 32 | ], 33 | "iss" => "http://example.org", 34 | "sub" => "1234567890", 35 | ]; 36 | 37 | // default HS256 algorithm 38 | $token = jwt_encode($payload, $key); 39 | 40 | // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7Im5hbWUiOiJaaUhhbmcgR2FvIiwiYWRtaW4iOnRydWV9LCJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsInN1YiI6IjEyMzQ1Njc4OTAifQ.UcrCt9o9rz38kKMTa-nCrm7JNQRNAId5Xg9C7EIl2Zc 41 | echo $token; 42 | 43 | $decoded_token = jwt_decode($token, $key); 44 | 45 | // Array 46 | // ( 47 | // [data] => Array 48 | // ( 49 | // [name] => ZiHang Gao 50 | // [admin] => 1 51 | // ) 52 | // 53 | // [iss] => http://example.org 54 | // [sub] => 1234567890 55 | // ) 56 | print_r($decoded_token); 57 | 58 | // or would you prefer to use a static method call 59 | $token = \Cdoco\JWT::encode($payload, $key); 60 | $decoded_token = \Cdoco\JWT::decode($token, $key); 61 | ``` 62 | 63 | ## Algorithms and Usage 64 | 65 | The JWT supports NONE, HMAC, RSASSA and ECDSA algorithms for cryptographic signing. 66 | 67 | #### NONE 68 | 69 | - none - unsigned token 70 | 71 | ```php 72 | $payload = ['data' => 'test']; 73 | 74 | // IMPORTANT: set null as key parameter 75 | $token = jwt_encode($payload, null, 'none'); 76 | 77 | // eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9. 78 | echo $token; 79 | 80 | // Set key to nil and options to false otherwise this won't work 81 | $decoded_token = jwt_decode($token, null, false); 82 | 83 | // Array 84 | // ( 85 | // [data] => test 86 | // ) 87 | print_r($decoded_token); 88 | ``` 89 | 90 | #### HMAC (default: HS256) 91 | 92 | - HS256 - HMAC using SHA-256 hash algorithm (default) 93 | - HS384 - HMAC using SHA-384 hash algorithm 94 | - HS512 - HMAC using SHA-512 hash algorithm 95 | 96 | ```php 97 | $hmackey = "example-hmac-key"; 98 | 99 | $token = jwt_encode($payload, $hmackey, 'HS256'); 100 | 101 | // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.C8kzOqBbcaPRhRdLWdNVSvYkIPIBPu7f_8-avoG-JiU 102 | echo $token; 103 | 104 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 105 | 106 | // Array 107 | // ( 108 | // [data] => test 109 | // ) 110 | print_r($decoded_token); 111 | ``` 112 | 113 | #### RSA 114 | 115 | - RS256 - RSA using SHA-256 hash algorithm 116 | - RS384 - RSA using SHA-384 hash algorithm 117 | - RS512 - RSA using SHA-512 hash algorithm 118 | 119 | ```php 120 | $privateKey = file_get_contents('key/rsa_private_key.pem'); 121 | $publicKey = file_get_contents('key/rsa_public_key.pem'); 122 | 123 | $token = jwt_encode($payload, $privateKey, 'RS256'); 124 | 125 | // eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pkpKlkzQWSkme42WcOxwkLeUttiLeNORzthSJeIt140iNEtRK_f8IotoinfIKI7Y6x8pfQ4n1DHJ_5IUDe6elds8gnhLwfq5XRY48BGc8Dc_QowVQd75m5fXI6nFySW8z8CAsbwn2Efg-p7SLdfhWpNQ9AISfwa_1l-OB3BgKFw 126 | echo $token; 127 | 128 | $decoded_token = jwt_decode($token, $publicKey, ['algorithm' => 'RS256']); 129 | 130 | // Array 131 | // ( 132 | // [data] => test 133 | // ) 134 | print_r($decoded_token); 135 | ``` 136 | 137 | #### ECDSA 138 | 139 | - ES256 - ECDSA using P-256 and SHA-256 140 | - ES384 - ECDSA using P-384 and SHA-384 141 | - ES512 - ECDSA using P-521 and SHA-512 142 | 143 | ```php 144 | $privateKey = file_get_contents('key/ec_private_key.pem'); 145 | $publicKey = file_get_contents('key/ec_public_key.pem'); 146 | 147 | $token = jwt_encode($payload, $privateKey, 'ES256'); 148 | 149 | // eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.etzxzSvJi1QS5nUtKDuLX2sScZ5W50CJL6PivKys45nc77QLxnLsF5QQApEAis8SI28rqwP9VITqPPlwJBNdH3N5n0I58z3jevGJYOfRtBnCa6omUNE03nxoEYMqRBuP 150 | echo $token; 151 | 152 | $decoded_token = jwt_decode($token, $publicKey, ['algorithm' => 'ES256']); 153 | 154 | // Array 155 | // ( 156 | // [data] => test 157 | // ) 158 | print_r($decoded_token); 159 | ``` 160 | 161 | ## Support for reserved claim names 162 | 163 | JSON Web Token defines some reserved claim names and defines how they should be used. JWT supports these reserved claim names: 164 | 165 | - 'exp' (Expiration Time) Claim 166 | - 'nbf' (Not Before Time) Claim 167 | - 'iss' (Issuer) Claim 168 | - 'aud' (Audience) Claim 169 | - 'jti' (JWT ID) Claim 170 | - 'iat' (Issued At) Claim 171 | - 'sub' (Subject) Claim 172 | 173 | ### Expiration Time Claim 174 | 175 | > The `exp` (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the `exp` claim requires that the current date/time MUST be before the expiration date/time listed in the `exp` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a **NumericDate** value. Use of this claim is OPTIONAL. 176 | 177 | #### Handle Expiration Claim 178 | 179 | ```php 180 | $payload = ['data' => 'data', 'exp' => time() + 4 * 3600]; 181 | 182 | $token = jwt_encode($payload, $hmackey, 'HS256'); 183 | 184 | try { 185 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 186 | } catch (ExpiredSignatureException $e) { 187 | // Expired token 188 | } 189 | ``` 190 | 191 | #### Adding Leeway 192 | 193 | ```php 194 | $payload = ['data' => 'data', 'exp' => time() - 10]; 195 | 196 | // build expired token 197 | $token = jwt_encode($payload, $hmackey, 'HS256'); 198 | 199 | try { 200 | $decoded_token = jwt_decode($token, $hmackey, ['leeway' => 30, 'algorithm' => 'HS256']); 201 | } catch (ExpiredSignatureException $e) { 202 | // Expired token 203 | } 204 | ``` 205 | 206 | ### Not Before Time Claim 207 | 208 | > The `nbf` (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the `nbf` claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the `nbf` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a **NumericDate** value. Use of this claim is OPTIONAL. 209 | 210 | #### Handle Not Before Claim 211 | 212 | ```php 213 | $payload = ['data' => 'data', 'nbf' => time() - 3600]; 214 | 215 | $token = jwt_encode($payload, $hmackey, 'HS256'); 216 | 217 | try { 218 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 219 | } catch (BeforeValidException $e) { 220 | // Handle invalid token 221 | } 222 | ``` 223 | 224 | #### Adding Leeway 225 | 226 | ```php 227 | $payload = ['data' => 'data', 'nbf' => time() + 10]; 228 | 229 | // build expired token 230 | $token = jwt_encode($payload, $hmackey, 'HS256'); 231 | 232 | try { 233 | $decoded_token = jwt_decode($token, $hmackey, ['leeway' => 30, 'algorithm' => 'HS256']); 234 | } catch (BeforeValidException $e) { 235 | // Handle invalid token 236 | } 237 | ``` 238 | 239 | ### Issuer Claim 240 | 241 | > The `iss` (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The `iss` value is a case-sensitive string containing a **StringOrURI** value. Use of this claim is OPTIONAL. 242 | 243 | ```php 244 | $payload = ['data' => 'data', 'iss' => 'http://example.org']; 245 | 246 | $token = jwt_encode($payload, $hmackey, 'HS256'); 247 | 248 | try { 249 | $decoded_token = jwt_decode($token, $hmackey, ['iss' => 'http://example.org', 'algorithm' => 'HS256']); 250 | } catch (InvalidIssuerException $e) { 251 | // Handle invalid token 252 | } 253 | ``` 254 | 255 | ### Audience Claim 256 | 257 | > The `aud` (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the `aud` claim when this claim is present, then the JWT MUST be rejected. In the general case, the `aud` value is an array of case-sensitive strings, each containing a **StringOrURI** value. In the special case when the JWT has one audience, the `aud` value MAY be a single case-sensitive string containing a **StringOrURI** value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. 258 | 259 | ```php 260 | $payload = ['data' => 'data', 'aud' => ['Young', 'Old']]; 261 | 262 | $token = jwt_encode($payload, $hmackey, 'HS256'); 263 | 264 | try { 265 | $decoded_token = jwt_decode($token, $hmackey, ['aud' => ['Young', 'Old'], 'algorithm' => 'HS256']); 266 | } catch (InvalidAudException $e) { 267 | // Handle invalid token 268 | } 269 | ``` 270 | 271 | ### JWT ID Claim 272 | 273 | > The `jti` (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The `jti` claim can be used to prevent the JWT from being replayed. The `jti` value is a **case-sensitive string**. Use of this claim is OPTIONAL. 274 | 275 | ```php 276 | $payload = ['data' => 'data', 'jti' => md5('id')]; 277 | 278 | $token = jwt_encode($payload, $hmackey, 'HS256'); 279 | 280 | try { 281 | $decoded_token = jwt_decode($token, $hmackey, ['jti' => md5('id'), 'algorithm' => 'HS256']); 282 | } catch (InvalidJtiException $e) { 283 | // Handle invalid token 284 | } 285 | ``` 286 | 287 | ### Issued At Claim 288 | 289 | > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a **NumericDate** value. Use of this claim is OPTIONAL. 290 | 291 | ```php 292 | $payload = ['data' => 'data', 'iat' => time()]; 293 | 294 | $token = jwt_encode($payload, $hmackey, 'HS256'); 295 | 296 | try { 297 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 298 | } catch (InvalidIatException $e) { 299 | // Handle invalid token 300 | } 301 | ``` 302 | 303 | ### Subject Claim 304 | 305 | > The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a **StringOrURI** value. Use of this claim is OPTIONAL. 306 | 307 | ```php 308 | $payload = ['data' => 'data', 'sub' => 'Subject']; 309 | 310 | $token = jwt_encode($payload, $hmackey, 'HS256'); 311 | 312 | try { 313 | $decoded_token = jwt_decode($token, $hmackey, ['sub' => 'Subject', 'algorithm' => 'HS256']); 314 | } catch (InvalidSubException $e) { 315 | // Handle invalid token 316 | } 317 | ``` 318 | 319 | ## Benchmarks 320 | 321 | ![Benchmarks](https://cdoco.com/images/jwt-benchmarks.png "Benchmarks") 322 | 323 | ## Functions 324 | 325 | ```php 326 | // encode 327 | string jwt_encode(array $payload, string $key [, string $algorithm = 'HS256']) 328 | 329 | // decode 330 | array jwt_decode(string $token, string $key [, array $options = ['algorithm' => 'HS256']]) 331 | ``` 332 | 333 | ## IDE Helper 334 | 335 | ```php 336 | 376 | - 377 | - 378 | - 379 | 380 | ## License 381 | 382 | PHP License 3.01. See the [LICENSE](LICENSE) file. 383 | 384 | [travis-url]: https://travis-ci.org/cdoco/php-jwt 385 | [travis-image]: https://travis-ci.org/cdoco/php-jwt.svg 386 | -------------------------------------------------------------------------------- /config.m4: -------------------------------------------------------------------------------- 1 | dnl $Id$ 2 | dnl config.m4 for extension jwt 3 | 4 | PHP_ARG_ENABLE(jwt, whether to enable jwt support, 5 | [ --enable-jwt Enable jwt support]) 6 | 7 | PHP_ARG_WITH(openssl, whether to use OpenSSL library, 8 | [ --with-openssl[=DIR] Ignore presence of OpenSSL library (requires OpenSSL >= 1.1.0j)]) 9 | 10 | if test "$PHP_JWT" != "no"; then 11 | 12 | SEARCH_PATH="/usr/local /usr /usr/local/opt" 13 | SEARCH_FOR="/include/openssl/hmac.h" 14 | if test -r $PHP_OPENSSL/$SEARCH_FOR; then 15 | OPENSSL_DIR=$PHP_OPENSSL 16 | else 17 | AC_MSG_CHECKING([for OpenSSL library in default path]) 18 | for i in $SEARCH_PATH ; do 19 | if test -r $i/$SEARCH_FOR; then 20 | OPENSSL_DIR=$i 21 | AC_MSG_RESULT(found in $i) 22 | fi 23 | done 24 | fi 25 | 26 | if test -z "$OPENSSL_DIR"; then 27 | AC_MSG_RESULT([OpenSSL library not found]) 28 | AC_MSG_ERROR([Please reinstall the OpenSSL library]) 29 | fi 30 | 31 | PHP_ADD_INCLUDE($OPENSSL_DIR/include) 32 | 33 | AC_CHECK_HEADERS([openssl/hmac.h openssl/evp.h]) 34 | PHP_CHECK_LIBRARY(crypto, EVP_sha512, 35 | [ 36 | PHP_ADD_INCLUDE($OPENSSL_DIR/include) 37 | PHP_ADD_LIBRARY_WITH_PATH(crypto, $OPENSSL_DIR/lib, JWT_SHARED_LIBADD) 38 | ],[ 39 | AC_MSG_ERROR(wrong OpenSSL library version) 40 | ],[ 41 | -L$OPENSSL_DIR/lib -lcrypto 42 | ]) 43 | 44 | PHP_SUBST(JWT_SHARED_LIBADD) 45 | PHP_ADD_EXTENSION_DEP(jwt, json) 46 | PHP_NEW_EXTENSION(jwt, jwt.c openssl.c, $ext_shared) 47 | fi 48 | -------------------------------------------------------------------------------- /config.w32: -------------------------------------------------------------------------------- 1 | // $Id$ 2 | // vim:ft=javascript 3 | 4 | ARG_WITH("openssl", "OpenSSL support", "no,shared"); 5 | 6 | if (PHP_JWT != "no") { 7 | var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); 8 | 9 | if (ret > 0) { 10 | EXTENSION("jwt", "jwt.c openssl.c", PHP_EXTNAME_SHARED); 11 | } 12 | } -------------------------------------------------------------------------------- /example/ecdsa.php: -------------------------------------------------------------------------------- 1 | [ 22 | "name" => "ZiHang Gao", 23 | "admin" => true 24 | ], 25 | "iss" => "http://example.org", 26 | "sub" => "1234567890", 27 | ); 28 | 29 | $token = jwt_encode($payload, $privateKey, 'ES256'); 30 | 31 | echo $token . PHP_EOL; 32 | print_r(jwt_decode($token, $publicKey, ['algorithm' => 'ES256'])); -------------------------------------------------------------------------------- /example/hmac.php: -------------------------------------------------------------------------------- 1 | [ 6 | "name" => "ZiHang Gao", 7 | "admin" => true 8 | ], 9 | "iss" => "http://example.org", 10 | "sub" => "1234567890", 11 | ); 12 | 13 | // default HS256 algorithm 14 | $token = jwt_encode($payload, $key); 15 | 16 | echo $token . PHP_EOL; 17 | print_r(jwt_decode($token, $key)); -------------------------------------------------------------------------------- /example/none.php: -------------------------------------------------------------------------------- 1 | [ 5 | "name" => "ZiHang Gao", 6 | "admin" => true 7 | ], 8 | "iss" => "http://example.org", 9 | "sub" => "1234567890", 10 | ); 11 | 12 | // none algorithm 13 | $token = jwt_encode($payload, null, 'none'); 14 | 15 | echo $token . PHP_EOL; 16 | print_r(jwt_decode($token, null, false)); -------------------------------------------------------------------------------- /example/rsa.php: -------------------------------------------------------------------------------- 1 | [ 33 | "name" => "ZiHang Gao", 34 | "admin" => true 35 | ], 36 | "iss" => "http://example.org", 37 | "sub" => "1234567890", 38 | ); 39 | 40 | $token = jwt_encode($payload, $privateKey, 'RS256'); 41 | 42 | echo $token . PHP_EOL; 43 | print_r(jwt_decode($token, $publicKey, ['algorithm' => 'RS256'])); -------------------------------------------------------------------------------- /jwt.c: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | PHP Version 7 | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) 1997-2017 The PHP Group | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: ZiHang Gao | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | 19 | /* $Id$ */ 20 | 21 | #ifdef HAVE_CONFIG_H 22 | #include "config.h" 23 | #endif 24 | 25 | #include 26 | 27 | /* OpenSSL includes */ 28 | #include 29 | 30 | /* Exceptions */ 31 | static zend_class_entry *jwt_signature_invalid_cex; 32 | static zend_class_entry *jwt_before_valid_cex; 33 | static zend_class_entry *jwt_expired_signature_cex; 34 | static zend_class_entry *jwt_invalid_issuer_cex; 35 | static zend_class_entry *jwt_invalid_aud_cex; 36 | static zend_class_entry *jwt_invalid_jti_cex; 37 | static zend_class_entry *jwt_invalid_iat_cex; 38 | static zend_class_entry *jwt_invalid_sub_cex; 39 | 40 | static zend_class_entry *jwt_ce; 41 | 42 | ZEND_DECLARE_MODULE_GLOBALS(jwt) 43 | 44 | ZEND_BEGIN_ARG_INFO_EX(arginfo_jwt_encode, 0, 0, 2) 45 | ZEND_ARG_ARRAY_INFO(0, payload, 1) 46 | ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 1) 47 | ZEND_ARG_TYPE_INFO(0, alg, IS_STRING, 1) 48 | ZEND_END_ARG_INFO() 49 | 50 | ZEND_BEGIN_ARG_INFO_EX(arginfo_jwt_decode, 0, 0, 2) 51 | ZEND_ARG_TYPE_INFO(0, token, IS_STRING, 1) 52 | ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 1) 53 | ZEND_ARG_INFO(0, options) 54 | ZEND_END_ARG_INFO() 55 | 56 | /* register internal class */ 57 | static zend_class_entry *jwt_register_class(const char *name) 58 | { 59 | zend_class_entry ce; 60 | 61 | INIT_CLASS_ENTRY_EX(ce, name, strlen(name), NULL); 62 | return zend_register_internal_class_ex(&ce, zend_ce_exception); 63 | } 64 | 65 | /* string to algorithm */ 66 | jwt_alg_t jwt_str_alg(const char *alg) 67 | { 68 | if (alg == NULL) 69 | return JWT_ALG_INVAL; 70 | 71 | if (!strcasecmp(alg, "none")) 72 | return JWT_ALG_NONE; 73 | else if (!strcasecmp(alg, "HS256")) 74 | return JWT_ALG_HS256; 75 | else if (!strcasecmp(alg, "HS384")) 76 | return JWT_ALG_HS384; 77 | else if (!strcasecmp(alg, "HS512")) 78 | return JWT_ALG_HS512; 79 | else if (!strcasecmp(alg, "RS256")) 80 | return JWT_ALG_RS256; 81 | else if (!strcasecmp(alg, "RS384")) 82 | return JWT_ALG_RS384; 83 | else if (!strcasecmp(alg, "RS512")) 84 | return JWT_ALG_RS512; 85 | else if (!strcasecmp(alg, "ES256")) 86 | return JWT_ALG_ES256; 87 | else if (!strcasecmp(alg, "ES384")) 88 | return JWT_ALG_ES384; 89 | else if (!strcasecmp(alg, "ES512")) 90 | return JWT_ALG_ES512; 91 | 92 | return JWT_ALG_INVAL; 93 | } 94 | 95 | /* jwt sign */ 96 | static int jwt_sign(jwt_t *jwt, char **out, unsigned int *len) 97 | { 98 | switch (jwt->alg) { 99 | /* HMAC */ 100 | case JWT_ALG_HS256: 101 | case JWT_ALG_HS384: 102 | case JWT_ALG_HS512: 103 | return jwt_sign_sha_hmac(jwt, out, len); 104 | 105 | /* RSA */ 106 | case JWT_ALG_RS256: 107 | case JWT_ALG_RS384: 108 | case JWT_ALG_RS512: 109 | 110 | /* ECC */ 111 | case JWT_ALG_ES256: 112 | case JWT_ALG_ES384: 113 | case JWT_ALG_ES512: 114 | return jwt_sign_sha_pem(jwt, out, len); 115 | 116 | /* You wut, mate? */ 117 | default: 118 | return EINVAL; 119 | } 120 | } 121 | 122 | /* jwt verify */ 123 | static int jwt_verify(jwt_t *jwt, const char *sig) 124 | { 125 | switch (jwt->alg) { 126 | /* HMAC */ 127 | case JWT_ALG_HS256: 128 | case JWT_ALG_HS384: 129 | case JWT_ALG_HS512: 130 | return jwt_verify_sha_hmac(jwt, sig); 131 | 132 | /* RSA */ 133 | case JWT_ALG_RS256: 134 | case JWT_ALG_RS384: 135 | case JWT_ALG_RS512: 136 | 137 | /* ECC */ 138 | case JWT_ALG_ES256: 139 | case JWT_ALG_ES384: 140 | case JWT_ALG_ES512: 141 | return jwt_verify_sha_pem(jwt, sig); 142 | 143 | /* You wut, mate? */ 144 | default: 145 | return EINVAL; 146 | } 147 | } 148 | 149 | /* jwt new */ 150 | int jwt_new(jwt_t **jwt) 151 | { 152 | if (!jwt) { 153 | return EINVAL; 154 | } 155 | 156 | *jwt = emalloc(sizeof(jwt_t)); 157 | if (!*jwt) { 158 | return ENOMEM; 159 | } 160 | 161 | memset(*jwt, 0, sizeof(jwt_t)); 162 | 163 | return 0; 164 | } 165 | 166 | /* jwt free */ 167 | void jwt_free(jwt_t *jwt) 168 | { 169 | if (!jwt) { 170 | return; 171 | } 172 | 173 | efree(jwt); 174 | } 175 | 176 | /* base64 url safe encode */ 177 | void jwt_b64_url_encode_ex(char *str) 178 | { 179 | int len = strlen(str); 180 | int i, t; 181 | 182 | for (i = t = 0; i < len; i++) { 183 | switch (str[i]) { 184 | case '+': 185 | str[t++] = '-'; 186 | break; 187 | case '/': 188 | str[t++] = '_'; 189 | break; 190 | case '=': 191 | break; 192 | default: 193 | str[t++] = str[i]; 194 | } 195 | } 196 | 197 | str[t] = '\0'; 198 | } 199 | 200 | /* base64 encode */ 201 | char *jwt_b64_url_encode(zend_string *input) 202 | { 203 | zend_string *b64_str = php_base64_encode((const unsigned char *)ZSTR_VAL(input), ZSTR_LEN(input)); 204 | 205 | /* replace str */ 206 | char *new = estrdup(ZSTR_VAL(b64_str)); 207 | jwt_b64_url_encode_ex(new); 208 | 209 | zend_string_free(b64_str); 210 | 211 | return new; 212 | } 213 | 214 | /* base64 decode */ 215 | zend_string *jwt_b64_url_decode(const char *src) 216 | { 217 | char *new; 218 | int len, i, z; 219 | 220 | /* Decode based on RFC-4648 URI safe encoding. */ 221 | len = strlen(src); 222 | new = alloca(len + 4); 223 | if (!new) { 224 | return NULL; 225 | } 226 | 227 | for (i = 0; i < len; i++) { 228 | switch (src[i]) { 229 | case '-': 230 | new[i] = '+'; 231 | break; 232 | case '_': 233 | new[i] = '/'; 234 | break; 235 | default: 236 | new[i] = src[i]; 237 | } 238 | } 239 | z = 4 - (i % 4); 240 | if (z < 4) { 241 | while (z--) { 242 | new[i++] = '='; 243 | } 244 | } 245 | new[i] = '\0'; 246 | 247 | /* base64 decode */ 248 | return php_base64_decode_ex((const unsigned char *)new, strlen(new), 1); 249 | } 250 | 251 | /* hash find string */ 252 | char *jwt_hash_str_find_str(zval *arr, char *key) 253 | { 254 | char *str = NULL; 255 | zval *zv = zend_hash_str_find(Z_ARRVAL_P(arr), key, strlen(key)); 256 | 257 | if (zv != NULL) { 258 | if (Z_TYPE_P(zv) == IS_STRING) { 259 | str = Z_STRVAL_P(zv); 260 | } else { 261 | php_error_docref(NULL, E_WARNING, "%s type must be string", key); 262 | } 263 | } 264 | 265 | return str; 266 | } 267 | 268 | /* hash find long */ 269 | long jwt_hash_str_find_long(zval *arr, char *key) 270 | { 271 | zval *zv = zend_hash_str_find(Z_ARRVAL_P(arr), key, strlen(key)); 272 | 273 | if (zv != NULL) { 274 | if (Z_TYPE_P(zv) == IS_LONG) { 275 | return Z_LVAL_P(zv); 276 | } else { 277 | php_error_docref(NULL, E_WARNING, "%s type must be long", key); 278 | } 279 | } 280 | 281 | return 0; 282 | } 283 | 284 | /* verify string claims */ 285 | int jwt_verify_claims_str(zval *arr, char *key, char *str) 286 | { 287 | char *rs = jwt_hash_str_find_str(arr, key); 288 | if (rs && str && strcmp(rs, str)) { 289 | return FAILURE; 290 | } 291 | 292 | return 0; 293 | } 294 | 295 | /* array equals */ 296 | int jwt_array_equals(zend_array *arr1, zend_array *arr2) { 297 | zend_ulong i; 298 | zval *value = NULL; 299 | 300 | if (arr1 && arr2) { 301 | if (zend_array_count(arr1) != zend_array_count(arr2)) { 302 | return FAILURE; 303 | } 304 | 305 | ZEND_HASH_FOREACH_NUM_KEY_VAL(arr1, i, value) { 306 | zval *tmp = zend_hash_index_find(arr2, i); 307 | 308 | if (value && tmp){ 309 | if (Z_TYPE_P(value) == IS_STRING && Z_TYPE_P(tmp) == IS_STRING) { 310 | if (strcmp(Z_STRVAL_P(value), Z_STRVAL_P(tmp))) { 311 | return FAILURE; 312 | } 313 | } else { 314 | php_error_docref(NULL, E_WARNING, "Aud each item type must be string"); 315 | } 316 | } 317 | } ZEND_HASH_FOREACH_END(); 318 | } 319 | 320 | return 0; 321 | } 322 | 323 | /* verify body */ 324 | int jwt_verify_body(char *body, zval *return_value) 325 | { 326 | zend_class_entry *ce; 327 | char *err_msg = NULL; 328 | time_t curr_time = time((time_t*)NULL); 329 | zend_string *vs = jwt_b64_url_decode(body); 330 | 331 | #define FORMAT_CEX_TIME(t, cex) do { \ 332 | struct tm *timeinfo; \ 333 | char buf[128]; \ 334 | timeinfo = localtime(&t); \ 335 | strftime(buf, sizeof(buf), "Cannot handle token prior to %Y-%m-%d %H:%M:%S", timeinfo); \ 336 | ce = cex; \ 337 | err_msg = buf; \ 338 | } while(0); 339 | 340 | #define FORMAT_CEX_MSG(msg, cex) do { \ 341 | ce = cex; \ 342 | err_msg = msg; \ 343 | } while(0); 344 | 345 | if (!vs) { 346 | FORMAT_CEX_MSG("Invalid body", spl_ce_UnexpectedValueException); 347 | goto done; 348 | } 349 | 350 | /* decode json to array */ 351 | php_json_decode_ex(return_value, ZSTR_VAL(vs), ZSTR_LEN(vs), PHP_JSON_OBJECT_AS_ARRAY, 512); 352 | zend_string_free(vs); 353 | 354 | if (Z_TYPE(*return_value) == IS_ARRAY) { 355 | /* set expiration and not before */ 356 | JWT_G(expiration) = jwt_hash_str_find_long(return_value, "exp"); 357 | JWT_G(not_before) = jwt_hash_str_find_long(return_value, "nbf"); 358 | JWT_G(iat) = jwt_hash_str_find_long(return_value, "iat"); 359 | 360 | /* expiration */ 361 | if (JWT_G(expiration) && (curr_time - JWT_G(leeway)) >= JWT_G(expiration)) 362 | FORMAT_CEX_MSG("Expired token", jwt_expired_signature_cex); 363 | /* not before */ 364 | if (JWT_G(not_before) && JWT_G(not_before) > (curr_time + JWT_G(leeway))) 365 | FORMAT_CEX_TIME(JWT_G(not_before), jwt_before_valid_cex); 366 | /* iat */ 367 | if (JWT_G(iat) && JWT_G(iat) > (curr_time + JWT_G(leeway))) 368 | FORMAT_CEX_TIME(JWT_G(iat), jwt_invalid_iat_cex); 369 | /* iss */ 370 | if (jwt_verify_claims_str(return_value, "iss", JWT_G(iss))) 371 | FORMAT_CEX_MSG("Invalid Issuer", jwt_invalid_issuer_cex); 372 | /* jti */ 373 | if (jwt_verify_claims_str(return_value, "jti", JWT_G(jti))) 374 | FORMAT_CEX_MSG("Invalid Jti", jwt_invalid_jti_cex); 375 | 376 | /* aud */ 377 | size_t flag = 0; 378 | zval *zv_aud = zend_hash_str_find(Z_ARRVAL_P(return_value), "aud", strlen("aud")); 379 | 380 | if (zv_aud && JWT_G(aud)) { 381 | switch(Z_TYPE_P(zv_aud)) { 382 | case IS_ARRAY: 383 | if (jwt_array_equals(Z_ARRVAL_P(JWT_G(aud)), Z_ARRVAL_P(zv_aud))) flag = 1; 384 | break; 385 | case IS_STRING: 386 | if (strcmp(Z_STRVAL_P(JWT_G(aud)), Z_STRVAL_P(zv_aud))) flag = 1; 387 | break; 388 | default: 389 | php_error_docref(NULL, E_WARNING, "Aud type must be string or array"); 390 | break; 391 | } 392 | 393 | if (flag) FORMAT_CEX_MSG("Invalid Aud", jwt_invalid_aud_cex); 394 | } 395 | 396 | /* sub */ 397 | if (jwt_verify_claims_str(return_value, "sub", JWT_G(sub))) 398 | FORMAT_CEX_MSG("Invalid Sub", jwt_invalid_sub_cex); 399 | } else { 400 | FORMAT_CEX_MSG("Json decode error", spl_ce_UnexpectedValueException); 401 | } 402 | 403 | done: 404 | if (err_msg) { 405 | zend_throw_exception(ce, err_msg, 0); 406 | return FAILURE; 407 | } 408 | 409 | return 0; 410 | } 411 | 412 | /* parse options */ 413 | int jwt_parse_options(zval *options) 414 | { 415 | /* check options */ 416 | if (options != NULL) { 417 | switch(Z_TYPE_P(options)) { 418 | case IS_ARRAY: 419 | { 420 | /* check algorithm */ 421 | char *alg = jwt_hash_str_find_str(options, "algorithm"); 422 | if (alg) { 423 | JWT_G(algorithm) = alg; 424 | } 425 | 426 | /* options */ 427 | JWT_G(leeway) = jwt_hash_str_find_long(options, "leeway"); 428 | JWT_G(iss) = jwt_hash_str_find_str(options, "iss"); 429 | JWT_G(jti) = jwt_hash_str_find_str(options, "jti"); 430 | JWT_G(aud) = zend_hash_str_find(Z_ARRVAL_P(options), "aud", strlen("aud")); 431 | JWT_G(sub) = jwt_hash_str_find_str(options, "sub"); 432 | } 433 | break; 434 | case IS_NULL: 435 | case IS_FALSE: 436 | JWT_G(algorithm) = "none"; 437 | break; 438 | default: 439 | break; 440 | } 441 | } 442 | 443 | return 0; 444 | } 445 | 446 | /* Jwt encode */ 447 | static void php_jwt_encode(INTERNAL_FUNCTION_PARAMETERS) { 448 | zval *payload = NULL, header; 449 | zend_string *key = NULL; 450 | smart_str json_header = {0}, json_payload = {0}; 451 | 452 | char *sig = NULL, *alg = "HS256", *buf = NULL; 453 | unsigned int sig_len; 454 | size_t alg_len; 455 | jwt_t *jwt = NULL; 456 | 457 | if (zend_parse_parameters(ZEND_NUM_ARGS(), "aS|s", &payload, &key, &alg, &alg_len) == FAILURE) { 458 | return; 459 | } 460 | 461 | /* init jwt */ 462 | jwt_new(&jwt); 463 | 464 | /* check algorithm */ 465 | jwt->alg = jwt_str_alg(alg); 466 | 467 | if (jwt->alg == JWT_ALG_INVAL) { 468 | zend_throw_exception(spl_ce_UnexpectedValueException, "Algorithm not supported", 0); 469 | goto encode_done; 470 | } 471 | 472 | /* init */ 473 | array_init(&header); 474 | 475 | /* JWT header array */ 476 | add_assoc_string(&header, "typ", "JWT"); 477 | add_assoc_string(&header, "alg", alg); 478 | 479 | /* json encode */ 480 | php_json_encode(&json_header, &header, 0); 481 | char *header_b64 = jwt_b64_url_encode(json_header.s); 482 | 483 | php_json_encode(&json_payload, payload, 0); 484 | char *payload_b64 = jwt_b64_url_encode(json_payload.s); 485 | 486 | zval_ptr_dtor(&header); 487 | smart_str_free(&json_header); 488 | smart_str_free(&json_payload); 489 | 490 | int buflen = strlen(header_b64) + strlen(payload_b64) + 2; 491 | buf = (char *)ecalloc(buflen, 1); 492 | strcpy(buf, header_b64); 493 | strcat(buf, "."); 494 | strcat(buf, payload_b64); 495 | 496 | efree(header_b64); 497 | efree(payload_b64); 498 | 499 | /* sign */ 500 | if (jwt->alg == JWT_ALG_NONE) { 501 | buflen += 1; 502 | /* alg none */ 503 | buf = (char *)erealloc(buf, buflen); 504 | strcat(buf, "."); 505 | buf[buflen] = '\0'; 506 | } else { 507 | /* set jwt struct */ 508 | jwt->key = key; 509 | jwt->str = zend_string_init(buf, strlen(buf), 0); 510 | 511 | /* sign */ 512 | if (jwt_sign(jwt, &sig, &sig_len)) { 513 | zend_throw_exception(spl_ce_DomainException, "OpenSSL unable to sign data", 0); 514 | zend_string_free(jwt->str); 515 | goto encode_done; 516 | } 517 | 518 | /* string concatenation */ 519 | zend_string *sig_str = zend_string_init(sig, sig_len, 0); 520 | char *sig_b64 = jwt_b64_url_encode(sig_str); 521 | 522 | buflen = strlen(sig_b64) + strlen(buf) + 2; 523 | char *tmp = (char *)ecalloc(buflen, 1); 524 | sprintf(tmp, "%s.%s", buf, sig_b64); 525 | 526 | efree(buf); 527 | buf = tmp; 528 | 529 | efree(sig_b64); 530 | zend_string_free(jwt->str); 531 | zend_string_free(sig_str); 532 | } 533 | 534 | encode_done: 535 | /* free */ 536 | if (sig) 537 | efree(sig); 538 | 539 | jwt_free(jwt); 540 | 541 | RETVAL_STRINGL(buf, strlen(buf)); 542 | efree(buf); 543 | } 544 | 545 | /* Jwt decode */ 546 | static void php_jwt_decode(INTERNAL_FUNCTION_PARAMETERS) { 547 | zend_string *token = NULL, *key = NULL; 548 | zval *options = NULL; 549 | smart_str buf = {0}; 550 | char *body = NULL, *sig = NULL; 551 | jwt_t *jwt = NULL; 552 | 553 | if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS|z", &token, &key, &options) == FAILURE) { 554 | return; 555 | } 556 | 557 | char *head = estrdup(ZSTR_VAL(token)); 558 | 559 | /* jwt init */ 560 | jwt_new(&jwt); 561 | 562 | /* Parse options */ 563 | if (jwt_parse_options(options) == FAILURE) { 564 | zend_throw_exception(spl_ce_UnexpectedValueException, "Options parse error", 0); 565 | goto decode_done; 566 | } 567 | 568 | /* Algorithm */ 569 | jwt->alg = jwt_str_alg(JWT_G(algorithm)); 570 | 571 | if (jwt->alg == JWT_ALG_INVAL) { 572 | zend_throw_exception(spl_ce_UnexpectedValueException, "Algorithm not supported", 0); 573 | goto decode_done; 574 | } 575 | 576 | /* Find the components. */ 577 | for (body = head; body[0] != '.'; body++) { 578 | if (body[0] == '\0') { 579 | goto decode_done; 580 | } 581 | } 582 | 583 | body[0] = '\0'; 584 | body++; 585 | 586 | for (sig = body; sig[0] != '.'; sig++) { 587 | if (sig[0] == '\0') { 588 | goto decode_done; 589 | } 590 | } 591 | 592 | sig[0] = '\0'; 593 | sig++; 594 | 595 | /* verify head */ 596 | zval zv; 597 | zend_string *json_h = jwt_b64_url_decode(head); 598 | 599 | if (!json_h) { 600 | zend_throw_exception(spl_ce_UnexpectedValueException, "Base64 decode error", 0); 601 | goto decode_done; 602 | } 603 | 604 | php_json_decode_ex(&zv, ZSTR_VAL(json_h), ZSTR_LEN(json_h), PHP_JSON_OBJECT_AS_ARRAY, 512); 605 | zend_string_free(json_h); 606 | 607 | if (Z_TYPE(zv) == IS_ARRAY) { 608 | zval *zalg = zend_hash_str_find(Z_ARRVAL(zv), "alg", strlen("alg")); 609 | 610 | zval_ptr_dtor(&zv); 611 | 612 | if (strcmp(Z_STRVAL_P(zalg), JWT_G(algorithm))) { 613 | zend_throw_exception(spl_ce_UnexpectedValueException, "Algorithm not allowed", 0); 614 | goto decode_done; 615 | } 616 | } else { 617 | zend_throw_exception(spl_ce_UnexpectedValueException, "Json decode error", 0); 618 | goto decode_done; 619 | } 620 | 621 | /* verify */ 622 | if (jwt->alg == JWT_ALG_NONE) { 623 | /* done */ 624 | } else { 625 | /* set jwt struct */ 626 | jwt->key = key; 627 | 628 | smart_str_appends(&buf, head); 629 | smart_str_appends(&buf, "."); 630 | smart_str_appends(&buf, body); 631 | 632 | jwt->str = buf.s; 633 | 634 | if (jwt_verify(jwt, sig)) { 635 | zend_throw_exception(jwt_signature_invalid_cex, "Signature verification failed", 0); 636 | goto decode_done; 637 | } 638 | } 639 | 640 | /* verify body */ 641 | if (jwt_verify_body(body, return_value) == FAILURE) { 642 | goto decode_done; 643 | } 644 | 645 | decode_done: 646 | efree(head); 647 | jwt_free(jwt); 648 | smart_str_free(&buf); 649 | } 650 | 651 | /* function jwt_encode() */ 652 | PHP_FUNCTION(jwt_encode) 653 | { 654 | php_jwt_encode(INTERNAL_FUNCTION_PARAM_PASSTHRU); 655 | } 656 | 657 | /* function jwt_decode() */ 658 | PHP_FUNCTION(jwt_decode) 659 | { 660 | php_jwt_decode(INTERNAL_FUNCTION_PARAM_PASSTHRU); 661 | } 662 | 663 | /* JWT::encode() */ 664 | PHP_METHOD(jwt, encode) 665 | { 666 | php_jwt_encode(INTERNAL_FUNCTION_PARAM_PASSTHRU); 667 | } 668 | 669 | /* JWT::decode() */ 670 | PHP_METHOD(jwt, decode) 671 | { 672 | php_jwt_decode(INTERNAL_FUNCTION_PARAM_PASSTHRU); 673 | } 674 | 675 | static const zend_function_entry jwt_functions[] = { 676 | PHP_FE(jwt_encode, arginfo_jwt_encode) 677 | PHP_FE(jwt_decode, arginfo_jwt_decode) 678 | PHP_FE_END 679 | }; 680 | 681 | static const zend_function_entry jwt_methods[] = { 682 | PHP_ME(jwt, encode, arginfo_jwt_encode, ZEND_ACC_STATIC | ZEND_ACC_PUBLIC) 683 | PHP_ME(jwt, decode, arginfo_jwt_decode, ZEND_ACC_STATIC | ZEND_ACC_PUBLIC) 684 | {NULL, NULL, NULL} 685 | }; 686 | 687 | /* GINIT */ 688 | PHP_GINIT_FUNCTION(jwt) { 689 | jwt_globals->expiration = 0; 690 | jwt_globals->not_before = 0; 691 | jwt_globals->iss = NULL; 692 | jwt_globals->iat = 0; 693 | jwt_globals->jti = NULL; 694 | jwt_globals->aud = NULL; 695 | jwt_globals->sub = NULL; 696 | jwt_globals->leeway = 0; 697 | jwt_globals->algorithm = "HS256"; 698 | } 699 | 700 | PHP_MINIT_FUNCTION(jwt) 701 | { 702 | zend_class_entry ce; 703 | 704 | INIT_CLASS_ENTRY(ce, "Cdoco\\JWT", jwt_methods); 705 | jwt_ce = zend_register_internal_class(&ce); 706 | 707 | /* register exception class */ 708 | jwt_signature_invalid_cex = jwt_register_class("SignatureInvalidException"); 709 | jwt_before_valid_cex = jwt_register_class("BeforeValidException"); 710 | jwt_expired_signature_cex = jwt_register_class("ExpiredSignatureException"); 711 | jwt_invalid_issuer_cex = jwt_register_class("InvalidIssuerException"); 712 | jwt_invalid_aud_cex = jwt_register_class("InvalidAudException"); 713 | jwt_invalid_jti_cex = jwt_register_class("InvalidJtiException"); 714 | jwt_invalid_iat_cex = jwt_register_class("InvalidIatException"); 715 | jwt_invalid_sub_cex = jwt_register_class("InvalidSubException"); 716 | 717 | return SUCCESS; 718 | } 719 | 720 | PHP_MSHUTDOWN_FUNCTION(jwt) 721 | { 722 | return SUCCESS; 723 | } 724 | 725 | PHP_MINFO_FUNCTION(jwt) 726 | { 727 | php_info_print_table_start(); 728 | php_info_print_table_header(2, "JWT support", "enabled"); 729 | php_info_print_table_row(2, "JWT Version", PHP_JWT_VERSION); 730 | php_info_print_table_row(2, "JWT Author", "ZiHang Gao "); 731 | php_info_print_table_row(2, "JWT Issues", "https://github.com/cdoco/php-jwt/issues"); 732 | 733 | /* openssl version info */ 734 | php_info_print_table_row(2, "OpenSSL Library Version", SSLeay_version(SSLEAY_VERSION)); 735 | php_info_print_table_row(2, "OpenSSL Header Version", OPENSSL_VERSION_TEXT); 736 | 737 | php_info_print_table_end(); 738 | } 739 | 740 | static const zend_module_dep jwt_deps[] = { 741 | ZEND_MOD_REQUIRED("json") 742 | ZEND_MOD_END 743 | }; 744 | 745 | zend_module_entry jwt_module_entry = { 746 | STANDARD_MODULE_HEADER_EX, NULL, 747 | jwt_deps, 748 | "jwt", 749 | jwt_functions, 750 | PHP_MINIT(jwt), 751 | PHP_MSHUTDOWN(jwt), 752 | NULL, /* Replace with NULL if there's nothing to do at request start */ 753 | NULL, /* Replace with NULL if there's nothing to do at request end */ 754 | PHP_MINFO(jwt), 755 | PHP_JWT_VERSION, 756 | PHP_MODULE_GLOBALS(jwt), 757 | PHP_GINIT(jwt), 758 | NULL, 759 | NULL, 760 | STANDARD_MODULE_PROPERTIES_EX 761 | }; 762 | 763 | #ifdef COMPILE_DL_JWT 764 | #ifdef ZTS 765 | ZEND_TSRMLS_CACHE_DEFINE() 766 | #endif 767 | ZEND_GET_MODULE(jwt) 768 | #endif 769 | 770 | /* 771 | * Local variables: 772 | * tab-width: 4 773 | * c-basic-offset: 4 774 | * End: 775 | * vim600: noet sw=4 ts=4 fdm=marker 776 | * vim<600: noet sw=4 ts=4 777 | */ 778 | -------------------------------------------------------------------------------- /jwt.php: -------------------------------------------------------------------------------- 1 | "; 3 | 4 | if(!extension_loaded('jwt')) { 5 | dl('jwt.' . PHP_SHLIB_SUFFIX); 6 | } 7 | 8 | use Cdoco\JWT; 9 | 10 | $key = "example_key"; 11 | $claims = array( 12 | "data" => [ 13 | "name" => "ZiHang Gao", 14 | "admin" => true 15 | ], 16 | "sub" => "1234567890", 17 | "nbf" => time() + 1, 18 | "aud" => 'yy', 19 | ); 20 | 21 | // default HS256 algorithm 22 | $token = JWT::encode($claims, $key); 23 | 24 | echo $token . PHP_EOL; 25 | print_r(JWT::decode($token, $key, ["audx" => 'yy', 'leeway' => 2, "iss" => "http://example.org"])); 26 | -------------------------------------------------------------------------------- /openssl.c: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | PHP Version 7 | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) 1997-2017 The PHP Group | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: ZiHang Gao | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | 19 | /* Copyright (C) 2015-2017 Ben Collins 20 | This file is part of the JWT C Library 21 | This Source Code Form is subject to the terms of the Mozilla Public 22 | License, v. 2.0. If a copy of the MPL was not distributed with this 23 | file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 24 | 25 | /* $Id$ */ 26 | 27 | #include 28 | 29 | /* OpenSSL includes */ 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | /* Routines to support crypto in JWT using OpenSSL. */ 37 | 38 | /* Functions to make libjwt backward compatible with OpenSSL version < 1.1.0 39 | * See https://wiki.openssl.org/index.php/1.1_API_Changes 40 | */ 41 | #if OPENSSL_VERSION_NUMBER < 0x10100000L 42 | 43 | static void ECDSA_SIG_get0(const ECDSA_SIG *sig, const BIGNUM **pr, const BIGNUM **ps) 44 | { 45 | if (pr != NULL) 46 | *pr = sig->r; 47 | if (ps != NULL) 48 | *ps = sig->s; 49 | } 50 | 51 | static int ECDSA_SIG_set0(ECDSA_SIG *sig, BIGNUM *r, BIGNUM *s) 52 | { 53 | if (r == NULL || s == NULL) 54 | return 0; 55 | 56 | BN_clear_free(sig->r); 57 | BN_clear_free(sig->s); 58 | sig->r = r; 59 | sig->s = s; 60 | 61 | return 1; 62 | } 63 | 64 | #endif 65 | 66 | int jwt_sign_sha_hmac(jwt_t *jwt, char **out, unsigned int *len) { 67 | 68 | const EVP_MD *alg; 69 | 70 | switch (jwt->alg) { 71 | /* HMAC */ 72 | case JWT_ALG_HS256: 73 | alg = EVP_sha256(); 74 | break; 75 | case JWT_ALG_HS384: 76 | alg = EVP_sha384(); 77 | break; 78 | case JWT_ALG_HS512: 79 | alg = EVP_sha512(); 80 | break; 81 | default: 82 | return EINVAL; 83 | } 84 | 85 | *out = emalloc(EVP_MAX_MD_SIZE); 86 | if (*out == NULL) { 87 | return ENOMEM; 88 | } 89 | 90 | HMAC(alg, ZSTR_VAL(jwt->key), ZSTR_LEN(jwt->key), 91 | (const unsigned char *)ZSTR_VAL(jwt->str), ZSTR_LEN(jwt->str), (unsigned char *)*out, 92 | len); 93 | 94 | return 0; 95 | } 96 | 97 | int jwt_verify_sha_hmac(jwt_t *jwt, const char *sig) 98 | { 99 | unsigned char res[EVP_MAX_MD_SIZE]; 100 | BIO *bmem = NULL, *b64 = NULL; 101 | unsigned int res_len; 102 | const EVP_MD *alg; 103 | char *buf; 104 | int len, ret = EINVAL; 105 | 106 | switch (jwt->alg) { 107 | case JWT_ALG_HS256: 108 | alg = EVP_sha256(); 109 | break; 110 | case JWT_ALG_HS384: 111 | alg = EVP_sha384(); 112 | break; 113 | case JWT_ALG_HS512: 114 | alg = EVP_sha512(); 115 | break; 116 | default: 117 | return EINVAL; 118 | } 119 | 120 | b64 = BIO_new(BIO_f_base64()); 121 | if (b64 == NULL) 122 | return ENOMEM; 123 | 124 | bmem = BIO_new(BIO_s_mem()); 125 | if (bmem == NULL) { 126 | BIO_free(b64); 127 | return ENOMEM; 128 | } 129 | 130 | BIO_push(b64, bmem); 131 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 132 | 133 | HMAC(alg, ZSTR_VAL(jwt->key), ZSTR_LEN(jwt->key), 134 | (const unsigned char *)ZSTR_VAL(jwt->str), ZSTR_LEN(jwt->str), res, &res_len); 135 | 136 | BIO_write(b64, res, res_len); 137 | 138 | (void)BIO_flush(b64); 139 | 140 | len = BIO_pending(bmem); 141 | if (len < 0) 142 | goto jwt_verify_hmac_done; 143 | 144 | buf = alloca(len + 1); 145 | if (!buf) { 146 | ret = ENOMEM; 147 | goto jwt_verify_hmac_done; 148 | } 149 | 150 | len = BIO_read(bmem, buf, len); 151 | buf[len] = '\0'; 152 | 153 | jwt_b64_url_encode_ex(buf); 154 | 155 | /* And now... */ 156 | ret = strcmp(buf, sig) ? EINVAL : 0; 157 | 158 | jwt_verify_hmac_done: 159 | BIO_free_all(b64); 160 | 161 | return ret; 162 | } 163 | 164 | #define SIGN_ERROR(__err) { ret = __err; goto jwt_sign_sha_pem_done; } 165 | 166 | int jwt_sign_sha_pem(jwt_t *jwt, char **out, unsigned int *len) 167 | { 168 | EVP_MD_CTX *mdctx = NULL; 169 | ECDSA_SIG *ec_sig = NULL; 170 | const BIGNUM *ec_sig_r = NULL; 171 | const BIGNUM *ec_sig_s = NULL; 172 | BIO *bufkey = NULL; 173 | const EVP_MD *alg; 174 | int type; 175 | EVP_PKEY *pkey = NULL; 176 | int pkey_type; 177 | unsigned char *sig; 178 | int ret = 0; 179 | size_t slen; 180 | 181 | switch (jwt->alg) { 182 | /* RSA */ 183 | case JWT_ALG_RS256: 184 | alg = EVP_sha256(); 185 | type = EVP_PKEY_RSA; 186 | break; 187 | case JWT_ALG_RS384: 188 | alg = EVP_sha384(); 189 | type = EVP_PKEY_RSA; 190 | break; 191 | case JWT_ALG_RS512: 192 | alg = EVP_sha512(); 193 | type = EVP_PKEY_RSA; 194 | break; 195 | 196 | /* ECC */ 197 | case JWT_ALG_ES256: 198 | alg = EVP_sha256(); 199 | type = EVP_PKEY_EC; 200 | break; 201 | case JWT_ALG_ES384: 202 | alg = EVP_sha384(); 203 | type = EVP_PKEY_EC; 204 | break; 205 | case JWT_ALG_ES512: 206 | alg = EVP_sha512(); 207 | type = EVP_PKEY_EC; 208 | break; 209 | 210 | default: 211 | return EINVAL; 212 | } 213 | 214 | bufkey = BIO_new_mem_buf(ZSTR_VAL(jwt->key), ZSTR_LEN(jwt->key)); 215 | if (bufkey == NULL) 216 | SIGN_ERROR(ENOMEM); 217 | 218 | /* This uses OpenSSL's default passphrase callback if needed. The 219 | * library caller can override this in many ways, all of which are 220 | * outside of the scope of LibJWT and this is documented in jwt.h. */ 221 | pkey = PEM_read_bio_PrivateKey(bufkey, NULL, NULL, NULL); 222 | if (pkey == NULL) 223 | SIGN_ERROR(EINVAL); 224 | 225 | pkey_type = EVP_PKEY_id(pkey); 226 | if (pkey_type != type) 227 | SIGN_ERROR(EINVAL); 228 | 229 | mdctx = EVP_MD_CTX_create(); 230 | if (mdctx == NULL) 231 | SIGN_ERROR(ENOMEM); 232 | 233 | /* Initialize the DigestSign operation using alg */ 234 | if (EVP_DigestSignInit(mdctx, NULL, alg, NULL, pkey) != 1) 235 | SIGN_ERROR(EINVAL); 236 | 237 | /* Call update with the message */ 238 | if (EVP_DigestSignUpdate(mdctx, ZSTR_VAL(jwt->str), ZSTR_LEN(jwt->str)) != 1) 239 | SIGN_ERROR(EINVAL); 240 | 241 | /* First, call EVP_DigestSignFinal with a NULL sig parameter to get length 242 | * of sig. Length is returned in slen */ 243 | if (EVP_DigestSignFinal(mdctx, NULL, &slen) != 1) 244 | SIGN_ERROR(EINVAL); 245 | 246 | /* Allocate memory for signature based on returned size */ 247 | sig = alloca(slen); 248 | if (sig == NULL) 249 | SIGN_ERROR(ENOMEM); 250 | 251 | /* Get the signature */ 252 | if (EVP_DigestSignFinal(mdctx, sig, &slen) != 1) 253 | SIGN_ERROR(EINVAL); 254 | 255 | if (pkey_type != EVP_PKEY_EC) { 256 | *out = emalloc(slen); 257 | if (*out == NULL) 258 | SIGN_ERROR(ENOMEM); 259 | memcpy(*out, sig, slen); 260 | *len = slen; 261 | } else { 262 | unsigned int degree, bn_len, r_len, s_len, buf_len; 263 | unsigned char *raw_buf; 264 | EC_KEY *ec_key; 265 | 266 | /* For EC we need to convert to a raw format of R/S. */ 267 | 268 | /* Get the actual ec_key */ 269 | ec_key = EVP_PKEY_get1_EC_KEY(pkey); 270 | if (ec_key == NULL) 271 | SIGN_ERROR(ENOMEM); 272 | 273 | degree = EC_GROUP_get_degree(EC_KEY_get0_group(ec_key)); 274 | 275 | EC_KEY_free(ec_key); 276 | 277 | /* Get the sig from the DER encoded version. */ 278 | ec_sig = d2i_ECDSA_SIG(NULL, (const unsigned char **)&sig, slen); 279 | if (ec_sig == NULL) 280 | SIGN_ERROR(ENOMEM); 281 | 282 | ECDSA_SIG_get0(ec_sig, &ec_sig_r, &ec_sig_s); 283 | r_len = BN_num_bytes(ec_sig_r); 284 | s_len = BN_num_bytes(ec_sig_s); 285 | bn_len = (degree + 7) / 8; 286 | if ((r_len > bn_len) || (s_len > bn_len)) 287 | SIGN_ERROR(EINVAL); 288 | 289 | buf_len = 2 * bn_len; 290 | raw_buf = alloca(buf_len); 291 | if (raw_buf == NULL) 292 | SIGN_ERROR(ENOMEM); 293 | 294 | /* Pad the bignums with leading zeroes. */ 295 | memset(raw_buf, 0, buf_len); 296 | BN_bn2bin(ec_sig_r, raw_buf + bn_len - r_len); 297 | BN_bn2bin(ec_sig_s, raw_buf + buf_len - s_len); 298 | 299 | *out = emalloc(buf_len); 300 | if (*out == NULL) 301 | SIGN_ERROR(ENOMEM); 302 | memcpy(*out, raw_buf, buf_len); 303 | *len = buf_len; 304 | } 305 | 306 | jwt_sign_sha_pem_done: 307 | if (bufkey) 308 | BIO_free(bufkey); 309 | if (pkey) 310 | EVP_PKEY_free(pkey); 311 | if (mdctx) 312 | EVP_MD_CTX_destroy(mdctx); 313 | if (ec_sig) 314 | ECDSA_SIG_free(ec_sig); 315 | 316 | return ret; 317 | } 318 | 319 | #define VERIFY_ERROR(__err) { ret = __err; goto jwt_verify_sha_pem_done; } 320 | 321 | int jwt_verify_sha_pem(jwt_t *jwt, const char *sig_b64) 322 | { 323 | unsigned char *sig = NULL; 324 | EVP_MD_CTX *mdctx = NULL; 325 | ECDSA_SIG *ec_sig = NULL; 326 | BIGNUM *ec_sig_r = NULL; 327 | BIGNUM *ec_sig_s = NULL; 328 | EVP_PKEY *pkey = NULL; 329 | const EVP_MD *alg; 330 | int type; 331 | int pkey_type; 332 | BIO *bufkey = NULL; 333 | int ret = 0; 334 | int slen; 335 | 336 | switch (jwt->alg) { 337 | /* RSA */ 338 | case JWT_ALG_RS256: 339 | alg = EVP_sha256(); 340 | type = EVP_PKEY_RSA; 341 | break; 342 | case JWT_ALG_RS384: 343 | alg = EVP_sha384(); 344 | type = EVP_PKEY_RSA; 345 | break; 346 | case JWT_ALG_RS512: 347 | alg = EVP_sha512(); 348 | type = EVP_PKEY_RSA; 349 | break; 350 | 351 | /* ECC */ 352 | case JWT_ALG_ES256: 353 | alg = EVP_sha256(); 354 | type = EVP_PKEY_EC; 355 | break; 356 | case JWT_ALG_ES384: 357 | alg = EVP_sha384(); 358 | type = EVP_PKEY_EC; 359 | break; 360 | case JWT_ALG_ES512: 361 | alg = EVP_sha512(); 362 | type = EVP_PKEY_EC; 363 | break; 364 | 365 | default: 366 | return EINVAL; 367 | } 368 | 369 | zend_string *sig_str = jwt_b64_url_decode(sig_b64); 370 | 371 | sig = (unsigned char *)ZSTR_VAL(sig_str); 372 | slen = ZSTR_LEN(sig_str); 373 | 374 | if (sig == NULL) 375 | VERIFY_ERROR(EINVAL); 376 | 377 | bufkey = BIO_new_mem_buf(ZSTR_VAL(jwt->key), ZSTR_LEN(jwt->key)); 378 | if (bufkey == NULL) 379 | VERIFY_ERROR(ENOMEM); 380 | 381 | /* This uses OpenSSL's default passphrase callback if needed. The 382 | * library caller can override this in many ways, all of which are 383 | * outside of the scope of LibJWT and this is documented in jwt.h. */ 384 | pkey = PEM_read_bio_PUBKEY(bufkey, NULL, NULL, NULL); 385 | if (pkey == NULL) 386 | VERIFY_ERROR(EINVAL); 387 | 388 | pkey_type = EVP_PKEY_id(pkey); 389 | if (pkey_type != type) 390 | VERIFY_ERROR(EINVAL); 391 | 392 | /* Convert EC sigs back to ASN1. */ 393 | if (pkey_type == EVP_PKEY_EC) { 394 | unsigned int degree, bn_len; 395 | unsigned char *p; 396 | EC_KEY *ec_key; 397 | 398 | ec_sig = ECDSA_SIG_new(); 399 | if (ec_sig == NULL) 400 | VERIFY_ERROR(ENOMEM); 401 | 402 | /* Get the actual ec_key */ 403 | ec_key = EVP_PKEY_get1_EC_KEY(pkey); 404 | if (ec_key == NULL) 405 | VERIFY_ERROR(ENOMEM); 406 | 407 | degree = EC_GROUP_get_degree(EC_KEY_get0_group(ec_key)); 408 | 409 | EC_KEY_free(ec_key); 410 | 411 | bn_len = (degree + 7) / 8; 412 | if ((bn_len * 2) != slen) 413 | VERIFY_ERROR(EINVAL); 414 | 415 | ec_sig_r = BN_bin2bn(sig, bn_len, NULL); 416 | ec_sig_s = BN_bin2bn(sig + bn_len, bn_len, NULL); 417 | if (ec_sig_r == NULL || ec_sig_s == NULL) 418 | VERIFY_ERROR(EINVAL); 419 | 420 | ECDSA_SIG_set0(ec_sig, ec_sig_r, ec_sig_s); 421 | efree(sig); 422 | 423 | slen = i2d_ECDSA_SIG(ec_sig, NULL); 424 | sig = emalloc(slen); 425 | if (sig == NULL) 426 | VERIFY_ERROR(ENOMEM); 427 | 428 | p = sig; 429 | slen = i2d_ECDSA_SIG(ec_sig, &p); 430 | 431 | if (slen == 0) 432 | VERIFY_ERROR(EINVAL); 433 | } 434 | 435 | mdctx = EVP_MD_CTX_create(); 436 | if (mdctx == NULL) 437 | VERIFY_ERROR(ENOMEM); 438 | 439 | /* Initialize the DigestVerify operation using alg */ 440 | if (EVP_DigestVerifyInit(mdctx, NULL, alg, NULL, pkey) != 1) 441 | VERIFY_ERROR(EINVAL); 442 | 443 | /* Call update with the message */ 444 | if (EVP_DigestVerifyUpdate(mdctx, ZSTR_VAL(jwt->str), ZSTR_LEN(jwt->str)) != 1) 445 | VERIFY_ERROR(EINVAL); 446 | 447 | /* Now check the sig for validity. */ 448 | if (EVP_DigestVerifyFinal(mdctx, sig, slen) != 1) { 449 | VERIFY_ERROR(EINVAL); 450 | } 451 | 452 | jwt_verify_sha_pem_done: 453 | if (bufkey) 454 | BIO_free(bufkey); 455 | if (pkey) 456 | EVP_PKEY_free(pkey); 457 | if (mdctx) 458 | EVP_MD_CTX_destroy(mdctx); 459 | if (sig) 460 | efree(sig); 461 | if (ec_sig) 462 | ECDSA_SIG_free(ec_sig); 463 | 464 | zend_string_free(sig_str); 465 | 466 | return ret; 467 | } -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | jwt 4 | pecl.php.net 5 | RFC 7519 OAuth JSON Web Token (JWT) implementation 6 | https://github.com/cdoco/php-jwt 7 | 8 | ZiHang Gao 9 | cdoco 10 | ocdoco@gmail.com 11 | yes 12 | 13 | 2018-06-15 14 | 15 | 16 | 0.2.3 17 | 0.2.3 18 | 19 | 20 | stable 21 | stable 22 | 23 | PHP 24 | 25 | - First release. 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 7.0.0 55 | 56 | 57 | 1.4.0 58 | 59 | 60 | 61 | jwt 62 | 63 | 64 | 65 | 66 | 0.2.3 67 | 0.2.3 68 | 69 | 70 | stable 71 | stable 72 | 73 | 2018-06-15 74 | 75 | PHP 76 | 77 | - First release. 78 | 79 | 80 | 81 | 82 | 0.2.4 83 | 0.2.4 84 | 85 | 86 | stable 87 | stable 88 | 89 | 2018-07-13 90 | 91 | PHP 92 | 93 | - Support JWT object. 94 | - Modify phpinfo display information. 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /php_jwt.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | PHP Version 7 | 4 | +----------------------------------------------------------------------+ 5 | | Copyright (c) 1997-2017 The PHP Group | 6 | +----------------------------------------------------------------------+ 7 | | This source file is subject to version 3.01 of the PHP license, | 8 | | that is bundled with this package in the file LICENSE, and is | 9 | | available through the world-wide-web at the following url: | 10 | | http://www.php.net/license/3_01.txt | 11 | | If you did not receive a copy of the PHP license and are unable to | 12 | | obtain it through the world-wide-web, please send a note to | 13 | | license@php.net so we can mail you a copy immediately. | 14 | +----------------------------------------------------------------------+ 15 | | Author: ZiHang Gao | 16 | +----------------------------------------------------------------------+ 17 | */ 18 | 19 | /* $Id$ */ 20 | 21 | #ifndef PHP_JWT_H 22 | #define PHP_JWT_H 23 | 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | extern zend_module_entry jwt_module_entry; 37 | #define phpext_jwt_ptr &jwt_module_entry 38 | 39 | #define PHP_JWT_VERSION "0.2.5" 40 | 41 | #ifdef ZTS 42 | #include "TSRM.h" 43 | #endif 44 | 45 | #define JWT_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(jwt, v) 46 | 47 | ZEND_BEGIN_MODULE_GLOBALS(jwt) 48 | time_t expiration; 49 | time_t not_before; 50 | char *iss; 51 | time_t iat; 52 | char *jti; 53 | zval *aud; 54 | char *sub; 55 | size_t leeway; 56 | char *algorithm; 57 | ZEND_END_MODULE_GLOBALS(jwt) 58 | 59 | /** JWT algorithm types. */ 60 | typedef enum jwt_alg { 61 | JWT_ALG_NONE = 0, 62 | JWT_ALG_HS256, 63 | JWT_ALG_HS384, 64 | JWT_ALG_HS512, 65 | JWT_ALG_RS256, 66 | JWT_ALG_RS384, 67 | JWT_ALG_RS512, 68 | JWT_ALG_ES256, 69 | JWT_ALG_ES384, 70 | JWT_ALG_ES512, 71 | JWT_ALG_TERM 72 | } jwt_alg_t; 73 | 74 | #define JWT_ALG_INVAL JWT_ALG_TERM 75 | 76 | /** Opaque JWT object. */ 77 | typedef struct jwt { 78 | jwt_alg_t alg; 79 | zend_string *key; 80 | zend_string *str; 81 | } jwt_t; 82 | 83 | char *jwt_b64_url_encode(zend_string *input); 84 | void jwt_b64_url_encode_ex(char *str); 85 | zend_string *jwt_b64_url_decode(const char *src); 86 | 87 | int jwt_sign_sha_hmac(jwt_t *jwt, char **out, unsigned int *len); 88 | int jwt_verify_sha_hmac(jwt_t *jwt, const char *sig); 89 | 90 | int jwt_sign_sha_pem(jwt_t *jwt, char **out, unsigned int *len); 91 | int jwt_verify_sha_pem(jwt_t *jwt, const char *sig_b64); 92 | 93 | #if defined(ZTS) && defined(COMPILE_DL_JWT) 94 | ZEND_TSRMLS_CACHE_EXTERN() 95 | #endif 96 | 97 | #endif /* PHP_JWT_H */ 98 | 99 | /* 100 | * Local variables: 101 | * tab-width: 4 102 | * c-basic-offset: 4 103 | * End: 104 | * vim600: noet sw=4 ts=4 fdm=marker 105 | * vim<600: noet sw=4 ts=4 106 | */ 107 | -------------------------------------------------------------------------------- /tests/001.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt presence 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 20 | --EXPECT-- 21 | jwt extension is available 22 | -------------------------------------------------------------------------------- /tests/002.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt HMAC algorithm (HS256) 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | [ 10 | "name" => "ZiHang Gao", 11 | "admin" => true 12 | ], 13 | "iss" => "http://example.org", 14 | "sub" => "1234567890", 15 | ); 16 | 17 | $token = jwt_encode($payload, $key); 18 | 19 | echo $token . PHP_EOL; 20 | print_r(jwt_decode($token, $key)); 21 | ?> 22 | --EXPECT-- 23 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7Im5hbWUiOiJaaUhhbmcgR2FvIiwiYWRtaW4iOnRydWV9LCJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsInN1YiI6IjEyMzQ1Njc4OTAifQ.6BafFmznKQOPVO9q5f5GgTVadITh2KmdUlJBF8UHOxo 24 | Array 25 | ( 26 | [data] => Array 27 | ( 28 | [name] => ZiHang Gao 29 | [admin] => 1 30 | ) 31 | 32 | [iss] => http://example.org 33 | [sub] => 1234567890 34 | ) 35 | -------------------------------------------------------------------------------- /tests/003.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt RSA algorithm (RS256) 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | [ 37 | "name" => "ZiHang Gao", 38 | "admin" => true 39 | ], 40 | "iss" => "http://example.org", 41 | "sub" => "1234567890", 42 | ); 43 | 44 | $token = jwt_encode($payload, $privateKey, 'RS256'); 45 | 46 | echo $token . PHP_EOL; 47 | print_r(jwt_decode($token, $publicKey, ['algorithm' => 'RS256'])); 48 | ?> 49 | --EXPECT-- 50 | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7Im5hbWUiOiJaaUhhbmcgR2FvIiwiYWRtaW4iOnRydWV9LCJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsInN1YiI6IjEyMzQ1Njc4OTAifQ.iSpiBRxW0RKDK0KZRDTwEQmllzkS-WQKMzQ8j3pSSA8NI3ukj0EjZCWIsWfgmb20xKViT5UhUAf_1UQwxwu5k0laEkJ8Ze04gj1P8KO--7BkxUFp4UerkDex49lwHSJmvTJBRZBy9t7BDqCaouEpUe0vZ6z_siMX95VMvVrk0g0 51 | Array 52 | ( 53 | [data] => Array 54 | ( 55 | [name] => ZiHang Gao 56 | [admin] => 1 57 | ) 58 | 59 | [iss] => http://example.org 60 | [sub] => 1234567890 61 | ) 62 | -------------------------------------------------------------------------------- /tests/004.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt ECDSA algorithm (ES256) 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | [ 26 | "name" => "ZiHang Gao", 27 | "admin" => true 28 | ], 29 | "iss" => "http://example.org", 30 | "sub" => "1234567890", 31 | ); 32 | 33 | $token = jwt_encode($payload, $privateKey, 'ES256'); 34 | print_r(jwt_decode($token, $publicKey, ['algorithm' => 'ES256'])); 35 | ?> 36 | --EXPECT-- 37 | Array 38 | ( 39 | [data] => Array 40 | ( 41 | [name] => ZiHang Gao 42 | [admin] => 1 43 | ) 44 | 45 | [iss] => http://example.org 46 | [sub] => 1234567890 47 | ) 48 | -------------------------------------------------------------------------------- /tests/005.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt NONE algorithm 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | [ 10 | "name" => "ZiHang Gao", 11 | "admin" => true 12 | ], 13 | "iss" => "http://example.org", 14 | "sub" => "1234567890", 15 | ); 16 | 17 | // none algorithm 18 | $token = jwt_encode($payload, null, 'none'); 19 | 20 | echo $token . PHP_EOL; 21 | print_r(jwt_decode($token, null, false)); 22 | ?> 23 | --EXPECT-- 24 | eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjp7Im5hbWUiOiJaaUhhbmcgR2FvIiwiYWRtaW4iOnRydWV9LCJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsInN1YiI6IjEyMzQ1Njc4OTAifQ. 25 | Array 26 | ( 27 | [data] => Array 28 | ( 29 | [name] => ZiHang Gao 30 | [admin] => 1 31 | ) 32 | 33 | [iss] => http://example.org 34 | [sub] => 1234567890 35 | ) 36 | -------------------------------------------------------------------------------- /tests/006.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt exp claim name (Expired token) 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'exp' => time() - 10]; 9 | $token = jwt_encode($payload, $hmackey, 'HS256'); 10 | 11 | try { 12 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 13 | } catch (ExpiredSignatureException $e) { 14 | // Expired token 15 | echo $e->getMessage() . "\n"; 16 | } 17 | 18 | try { 19 | $decoded_token = jwt_decode($token, $hmackey, ['leeway' => 30, 'algorithm' => 'HS256']); 20 | echo "Success\n"; 21 | } catch (ExpiredSignatureException $e) { 22 | // Expired token 23 | } 24 | ?> 25 | --EXPECT-- 26 | Expired token 27 | Success 28 | -------------------------------------------------------------------------------- /tests/007.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt nbf claim name 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'nbf' => time() + 10]; 9 | $token = jwt_encode($payload, $hmackey, 'HS256'); 10 | 11 | try { 12 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 13 | } catch (BeforeValidException $e) { 14 | // Expired token 15 | echo "FAIL\n"; 16 | } 17 | 18 | try { 19 | $decoded_token = jwt_decode($token, $hmackey, ['leeway' => 30, 'algorithm' => 'HS256']); 20 | echo "SUCCESS\n"; 21 | } catch (BeforeValidException $e) { 22 | // Expired token 23 | } 24 | ?> 25 | --EXPECT-- 26 | FAIL 27 | SUCCESS 28 | -------------------------------------------------------------------------------- /tests/008.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt iat claim name 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'iat' => time()]; 9 | $token = jwt_encode($payload, $hmackey, 'HS256'); 10 | 11 | try { 12 | $decoded_token = jwt_decode($token, $hmackey, ['algorithm' => 'HS256']); 13 | echo "SUCCESS\n"; 14 | } catch (InvalidIatException $e) { 15 | // Handle invalid token 16 | } 17 | ?> 18 | --EXPECT-- 19 | SUCCESS 20 | 21 | -------------------------------------------------------------------------------- /tests/009.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt iss claim name 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'iss' => 'http://example.org']; 9 | 10 | $token = jwt_encode($payload, $hmackey, 'HS256'); 11 | 12 | try { 13 | $decoded_token = jwt_decode($token, $hmackey, ['iss' => 'http://example.org', 'algorithm' => 'HS256']); 14 | echo "SUCCESS\n"; 15 | } catch (InvalidIssuerException $e) { 16 | // Handle invalid token 17 | } 18 | 19 | try { 20 | $decoded_token = jwt_decode($token, $hmackey, ['iss' => 'test', 'algorithm' => 'HS256']); 21 | } catch (InvalidIssuerException $e) { 22 | // Handle invalid token 23 | echo "FAIL\n"; 24 | } 25 | ?> 26 | --EXPECT-- 27 | SUCCESS 28 | FAIL 29 | -------------------------------------------------------------------------------- /tests/010.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt claim name 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'sub' => '1234567890']; 9 | 10 | $token = jwt_encode($payload, $hmackey); 11 | 12 | try { 13 | $decoded_token = jwt_decode($token, $hmackey, ['iss' => 'http://example.org']); 14 | echo "SUCCESS\n"; 15 | } catch (Exception $e) { 16 | // Expired token 17 | } 18 | ?> 19 | --EXPECT-- 20 | SUCCESS 21 | -------------------------------------------------------------------------------- /tests/011.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt aud claim name 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'aud' => ['Young', 'Old']]; 9 | 10 | $token = jwt_encode($payload, $hmackey, 'HS256'); 11 | 12 | try { 13 | $decoded_token = jwt_decode($token, $hmackey, ['aud' => ['Young', 'Old'], 'algorithm' => 'HS256']); 14 | echo "SUCCESS\n"; 15 | } catch (InvalidAudException $e) { 16 | // Handle invalid token 17 | } 18 | 19 | try { 20 | $decoded_token = jwt_decode($token, $hmackey, ['aud' => ['Young'], 'algorithm' => 'HS256']); 21 | } catch (InvalidAudException $e) { 22 | // Handle invalid token 23 | echo "FAIL\n"; 24 | } 25 | ?> 26 | --EXPECT-- 27 | SUCCESS 28 | FAIL 29 | -------------------------------------------------------------------------------- /tests/012.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt \Cdoco\JWT object 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | [ 12 | "name" => "ZiHang Gao", 13 | "admin" => true 14 | ], 15 | "iss" => "http://example.org", 16 | "sub" => "1234567890", 17 | ); 18 | 19 | $token = JWT::encode($payload, $key); 20 | 21 | echo $token . PHP_EOL; 22 | print_r(JWT::decode($token, $key)); 23 | ?> 24 | --EXPECT-- 25 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7Im5hbWUiOiJaaUhhbmcgR2FvIiwiYWRtaW4iOnRydWV9LCJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsInN1YiI6IjEyMzQ1Njc4OTAifQ.6BafFmznKQOPVO9q5f5GgTVadITh2KmdUlJBF8UHOxo 26 | Array 27 | ( 28 | [data] => Array 29 | ( 30 | [name] => ZiHang Gao 31 | [admin] => 1 32 | ) 33 | 34 | [iss] => http://example.org 35 | [sub] => 1234567890 36 | ) 37 | -------------------------------------------------------------------------------- /tests/013.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for jwt aud claim name (string type) 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'data', 'aud' => 'Young']; 9 | 10 | $token = jwt_encode($payload, $hmackey, 'HS256'); 11 | 12 | try { 13 | $decoded_token = jwt_decode($token, $hmackey, ['aud' => 'Young', 'algorithm' => 'HS256']); 14 | echo "SUCCESS\n"; 15 | } catch (InvalidAudException $e) { 16 | // Handle invalid token 17 | } 18 | 19 | try { 20 | $decoded_token = jwt_decode($token, $hmackey, ['aud' => 'young', 'algorithm' => 'HS256']); 21 | } catch (InvalidAudException $e) { 22 | // Handle invalid token 23 | echo "FAIL\n"; 24 | } 25 | ?> 26 | --EXPECT-- 27 | SUCCESS 28 | FAIL 29 | -------------------------------------------------------------------------------- /tests/014.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | ISSUE #18 expiration time bug 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'HS256']); 11 | } catch (ExpiredSignatureException $e) { 12 | // Handle expired token 13 | echo "FAIL\n"; 14 | } 15 | ?> 16 | --EXPECT-- 17 | FAIL 18 | -------------------------------------------------------------------------------- /tests/015.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | ISSUE #21 Segmentation fault of php-fpm instance on jwt_decode 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'HS256']); 11 | } catch (SignatureInvalidException $e) { 12 | // Handle expired token 13 | echo "FAIL\n"; 14 | } 15 | ?> 16 | --EXPECT-- 17 | FAIL 18 | -------------------------------------------------------------------------------- /tests/016.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | ISSUE #23 Segfault with multiple jwt_decode using RSA 3 | --SKIPIF-- 4 | 5 | --FILE-- 6 | 'sha512', 11 | 'private_key_bits' => 1024, 12 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 13 | ]); 14 | openssl_pkey_export($key, $private); 15 | $public = openssl_pkey_get_details($key)['key']; 16 | openssl_pkey_free($key); 17 | return [$public, $private]; 18 | } 19 | 20 | list($apub, $apriv) = generateKeyPair(); 21 | list($bpub, $bpriv) = generateKeyPair(); 22 | 23 | $payload = ['message' => 'hello world']; 24 | $token = jwt_encode($payload, $apriv, 'RS512'); 25 | $decoded = jwt_decode($token, $apub, ['algorithm' => 'RS512']); 26 | print_r($decoded); 27 | 28 | $payload = ['message' => 'hello world 2']; 29 | $token = jwt_encode($payload, $bpriv, 'RS512'); 30 | $decoded = jwt_decode($token, $bpub, ['algorithm' => 'RS512']); 31 | print_r($decoded); 32 | ?> 33 | --EXPECT-- 34 | Array 35 | ( 36 | [message] => hello world 37 | ) 38 | Array 39 | ( 40 | [message] => hello world 2 41 | ) 42 | -------------------------------------------------------------------------------- /travis/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | phpize && ./configure && make clean && make -------------------------------------------------------------------------------- /travis/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TEST_DIR="`pwd`/tests/" 3 | 4 | make test 5 | 6 | for file in `find $TEST_DIR -name "*.diff" 2>/dev/null` 7 | do 8 | grep "\-\-XFAIL--" ${file/%diff/phpt} >/dev/null 2>&1 9 | if [ $? -gt 0 ] 10 | then 11 | FAILS[${#FAILS[@]}]="$file" 12 | fi 13 | done 14 | 15 | if [ ${#FAILS[@]} -gt 0 ] 16 | then 17 | for fail in "${FAILS[@]}" 18 | do 19 | sh -xc "cat $fail" 20 | done 21 | exit 1 22 | else 23 | exit 0 24 | fi --------------------------------------------------------------------------------