├── .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 | 
3 | 
4 | 
5 | 
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 | 
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
--------------------------------------------------------------------------------