1+ <?php
2+
3+ define ('JWT_SUPPORTED_ALGORITHMS ' , array (
4+ 'ES384 ' => array ('openssl ' , 'SHA384 ' ),
5+ 'ES256 ' => array ('openssl ' , 'SHA256 ' ),
6+ 'HS256 ' => array ('hash_hmac ' , 'SHA256 ' ),
7+ 'HS384 ' => array ('hash_hmac ' , 'SHA384 ' ),
8+ 'HS512 ' => array ('hash_hmac ' , 'SHA512 ' ),
9+ 'RS256 ' => array ('openssl ' , 'SHA256 ' ),
10+ 'RS384 ' => array ('openssl ' , 'SHA384 ' ),
11+ 'RS512 ' => array ('openssl ' , 'SHA512 ' ),
12+ 'EdDSA ' => array ('sodium_crypto ' , 'EdDSA ' ),
13+ ));
14+
15+ define ('JWT_ASN1_SEQUENCE ' , 0x10 );
16+ define ('JWT_ASN1_INTEGER ' , 0x02 );
17+ define ('JWT_ASN1_BIT_STRING ' , 0x03 );
18+
19+ function _jwt_decode ($ base64 ){
20+ $ add = strlen ($ base64 ) % 4 ;
21+ if ($ add ) $ base64 .= str_repeat ("= " , 4 - $ add );
22+ $ base64 = base64_decode (str_replace (array ('- ' , '_ ' ), array ('+ ' , '/ ' ), $ base64 ));
23+ return json_decode ($ base64 , false , 512 , JSON_BIGINT_AS_STRING );
24+ }
25+
26+ function _jwt_encode ($ json ){
27+ $ json = json_encode ($ json );
28+ if (json_last_error () != JSON_ERROR_NONE ) throw new InvalidArgumentException (json_last_error_msg ());
29+ return str_replace (array ('= ' , '+ ' , '/ ' ), array ('' , '- ' , '_ ' , ), base64_encode ($ json ));
30+ }
31+
32+ function _jwt_read_der ($ der , $ offset =0 ){
33+ $ pos = $ offset ;
34+ $ size = strlen ($ der );
35+ $ constructed = (ord ($ der [$ pos ]) >> 5 ) & 0x01 ;
36+ $ type = ord ($ der [$ pos ++]) & 0x1f ;
37+ $ len = ord ($ der [$ pos ++]);
38+ if ($ len & 0x80 ){
39+ $ n = $ len & 0x1f ;
40+ $ len = 0 ;
41+ while ($ n -- && $ pos < $ size ){
42+ $ len = ($ len << 8 ) | ord ($ der [$ pos ++]);
43+ }
44+ }
45+ if ($ type == JWT_ASN1_BIT_STRING ){
46+ $ pos ++; // Skip the first contents octet (padding indicator)
47+ $ data = substr ($ der , $ pos , $ len - 1 );
48+ $ pos += $ len - 1 ;
49+ } else if (!$ constructed ){
50+ $ data = substr ($ der , $ pos , $ len );
51+ $ pos += $ len ;
52+ } else $ data = null ;
53+
54+ return array ($ pos , $ data );
55+ }
56+
57+ function _jwt_encode_der ($ type , $ value ){
58+ return chr ($ type | ($ type === JWT_ASN1_SEQUENCE ? 0x20 : 0 )) . chr (strlen ($ value ));
59+ }
60+
61+ function _jwt_decode_der ($ sig , $ keySize ){
62+ // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
63+ list ($ off , $ _ ) = _jwt_read_der ($ sig );
64+ list ($ off , $ r ) = _jwt_read_der ($ der , $ off );
65+ list ($ off , $ s ) = _jwt_read_der ($ der , $ off );
66+
67+ // Convert r-value and s-value from signed two's compliment to unsigned big-endian integers
68+ $ r = ltrim ($ r , "\x00" );
69+ $ s = ltrim ($ s , "\x00" );
70+
71+ // Pad out r and s so that they are $keySize bits long
72+ return str_pad ($ r , $ keySize / 8 , "\x00" , STR_PAD_LEFT ) . str_pad ($ s , $ keySize / 8 , "\x00" , STR_PAD_LEFT );
73+ }
74+
75+ function _jwt_strlen ($ str ){
76+ return function_exists ('mb_strlen ' ) ? mb_strlen ($ str , '8bit ' ) : strlen ($ str );
77+ }
78+
79+
80+ /** Verifies the integrity of a given message and signature
81+ * @param String $msg Message that should be verified
82+ * @param String $sig Signature of the message needed to prove the integrity
83+ * @param String $secretKey
84+ */
85+ function jwt_verify ($ msg , $ sig , $ secretKey , $ alg , $ algorithms =null ){
86+ $ algorithms = !empty ($ algorithms ) ? (is_array ($ algorithms ) ? $ algorithms : array ($ algorithms )) : JWT_SUPPORTED_ALGORITHMS ;
87+ if (empty ($ alg ) || !isset ($ algorithms [$ alg ])) return false ;
88+
89+ list ($ func , $ algo ) = $ algorithms [$ alg ];
90+ switch ($ func ) {
91+ case 'openssl ' :
92+ $ success = openssl_verify ($ msg , $ sig , $ secretKey , $ alg );
93+ if ($ success === 1 ) return true ;
94+ else if ($ success === 0 ) return false ;
95+ else throw new ErrorException (openssl_error_string ());
96+ case 'sodium_crypto ' :
97+ if (!function_exists ('sodium_crypto_sign_verify_detached ' )) throw new ErrorException ('libsodium is not available ' );
98+ try {
99+ // The last non-empty line is used as the key.
100+ $ lines = array_filter (explode ("\n" , $ secretKey ));
101+ $ key = base64_decode (end ($ lines ));
102+ return sodium_crypto_sign_verify_detached ($ sig , $ msg , $ secretKey );
103+ } catch (Exception $ e ) {
104+ throw new ErrorException ($ e ->getMessage (), 0 , $ e );
105+ }
106+ case 'hash_hmac ' :
107+ default :
108+ $ hash = hash_hmac ($ alg , $ msg , $ secretKey , true );
109+ if (function_exists ('hash_equals ' )) return hash_equals ($ sig , $ hash );
110+ $ sigLen = _jwt_strlen ($ sig );
111+ $ hashLen = _jwt_strlen ($ hash );
112+ $ len = min ($ sigLen , $ hashLen );
113+ $ status = 0 ;
114+ for ($ i = 0 ; $ i < $ len ; $ i ++) {
115+ $ status |= (ord ($ sig [$ i ]) ^ ord ($ hash [$ i ]));
116+ }
117+ $ status |= ($ sigLen ^ $ hashLen );
118+ return ($ status === 0 );
119+ }
120+ }
121+
122+
123+ /**
124+ * Decodes a JWT token
125+ * @param String $jwt JWT token that should be parsed
126+ * @param String $secretKey Private key to verify integrity of JWT (if null then $_ENV['JWT_SECRET_KEY'])
127+ * @param Int $currentTime Current UTC time seconds (optional, can be used for unit tests)
128+ * @param Array $algorithms Map of allowed algorithms (optional, if null or empty then JWT_SUPPORTED_ALGORITHMS will be used)
129+ * @return Object JSON object that was stored in the JWT or false if JWT invalid or expired
130+ */
131+ function jwt_decode ($ jwt , $ secretKey =null , $ currentTime =null , $ algorithms =null ){
132+ if (empty ($ secretKey )){
133+ if (!isset ($ _ENV ['JWT_SECRET_KEY ' ]) || empty ($ _ENV ['JWT_SECRET_KEY ' ]))
134+ throw new InvalidArgumentException ("Secret key cannot be null if \$_ENV['JWT_SECRET_KEY'] is not defined " );
135+ $ secretKey = $ _ENV ['JWT_SECRET_KEY ' ];
136+ }
137+ $ currentTime = $ currentTime ? $ currentTime : time ();
138+ $ algorithms = !empty ($ algorithms ) ? (is_array ($ algorithms ) ? $ algorithms : array ($ algorithms )) : JWT_SUPPORTED_ALGORITHMS ;
139+
140+ $ parts = explode ('. ' , $ jwt );
141+ if (count ($ parts ) != 3 ) return array ();
142+
143+ list ($ head64 , $ payload64 , $ sig64 ) = $ parts ;
144+ if (!($ header = _jwt_decode ($ head64 ) || !($ payload = _jwt_decode ($ payload64 ) || !($ sig = _jwt_decode ($ sig64 )))))
145+ return array ();
146+
147+ if (empty ($ header ->alg ) || !isset ($ algorithms [$ header ->alg ]))
148+ return array ();
149+
150+ if ($ header ->alg === 'ES256 ' || $ header ->alg === 'ES384 ' ) {
151+ // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
152+ list ($ r , $ s ) = str_split ($ raw , strlen ($ raw )/2 );
153+ $ r = ltrim ($ r , "\x00" );
154+ $ s = ltrim ($ s , "\x00" );
155+ $ r = _jwt_encode_der (JWT_ASN1_INTEGER , ord[$ r [0 ]] > 0x7f ? "\x00" .$ r : $ r );
156+ $ s = _jwt_encode_der (JWT_ASN1_INTEGER , ord[$ s [0 ]] > 0x7f ? "\x00" .$ s : $ s );
157+ $ sig = _jwt_encode_der (JWT_ASN1_SEQUENCE , $ r .$ s );
158+ }
159+
160+ // Check the signature
161+ if (!jwt_verify ($ headb64 .$ payload64 , $ sig , $ secretKey , $ header ->alg )) return array ();
162+
163+ // Check if token can already be used (if set)
164+ $ leeway = isset ($ _ENV ['JWT_LEEWAY_SEC ' ]) ? intval ($ _ENV ['JWT_LEEWAY_SEC ' ]) : 0 ;
165+ if (isset ($ payload ->nbf ) && $ payload ->nbf > ($ timestamp + $ leeway )) return false ;
166+
167+ // Check that token has been created before 'now'
168+ if (isset ($ payload ->iat ) && $ payload ->iat > ($ timestamp + $ leeway )) return false ;
169+
170+ // Check if this token has expired.
171+ if (isset ($ payload ->exp ) && ($ timestamp - $ leeway ) >= $ payload ->exp ) return false ;
172+
173+ return $ payload ;
174+ }
175+
176+
177+
178+ /** Creates a signature for a given string
179+ * @param String $msg Message the signature should be generated for
180+ * @param String $secretKey Private key to sign message (if null then $_ENV['JWT_SECRET_KEY'])
181+ * @param String $alg Algorithm that should be used to sign the string
182+ * @return String Signed message
183+ */
184+ function jwt_sign ($ msg , $ secretKey =null , $ alg ='HS256 ' ){
185+ if (empty ($ secretKey )){
186+ if (!isset ($ _ENV ['JWT_SECRET_KEY ' ]) || empty ($ _ENV ['JWT_SECRET_KEY ' ]))
187+ throw new InvalidArgumentException ("Secret key cannot be null if \$_ENV['JWT_SECRET_KEY'] is not defined " );
188+ $ secretKey = $ _ENV ['JWT_SECRET_KEY ' ];
189+ }
190+ if (empty ($ alg ) || JWT_SUPPORTED_ALGORITHMS [$ alg ]) throw new InvalidArgumentException ('Algorithm not supported ' );
191+ list ($ func , $ alg ) = JWT_SUPPORTED_ALGORITHMS [$ alg ];
192+ switch ($ func ) {
193+ case 'hash_hmac ' :
194+ return hash_hmac ($ alg , $ msg , $ secretKey , true );
195+ case 'openssl ' :
196+ $ sig = '' ;
197+ $ success = openssl_sign ($ msg , $ signature , $ secretKey , $ alg );
198+ if (!$ success ) throw new ErrorException ("OpenSSL unable to sign data " );
199+ if ($ alg === 'ES256 ' )
200+ $ sig = _jwt_decode_der ($ signature , 256 );
201+ else if ($ alg === 'ES384 ' )
202+ $ sig = _jwt_decode_der ($ signature , 384 );
203+ return $ sig ;
204+ case 'sodium_crypto ' :
205+ if (!function_exists ('sodium_crypto_sign_detached ' )) throw new ErrorException ('libsodium is not available ' );
206+ try {
207+ // The last non-empty line is used as the key.
208+ $ lines = array_filter (explode ("\n" , $ secretKey ));
209+ $ key = base64_decode (end ($ lines ));
210+ return sodium_crypto_sign_detached ($ msg , $ ksecretKeyey );
211+ } catch (Exception $ e ) {
212+ throw new ErrorException ($ e ->getMessage (), 0 , $ e );
213+ }
214+ }
215+ }
216+
217+
218+ /** Creates a JWT
219+ * @param Array $payload JSON object into which custom data can be stored
220+ * @param String $secretKey Private key to sign JWT (if null then $_ENV['JWT_SECRET_KEY'])
221+ * @param String $alg Algorithm that should be used for signing (optional)
222+ * @param String $keyId ID of the key (optional)
223+ * @param Array $head Additional JWT header fields (optional)
224+ * @return String JWT token
225+ */
226+ function jwt_encode ($ payload , $ secretKey =null , $ alg ='HS256 ' , $ keyId =null , $ head =null ){
227+ $ header = array ('typ ' => 'JWT ' , 'alg ' => $ alg );
228+ if ($ keyId !== null ) $ header ['kid ' ] = $ keyId ;
229+ if (isset ($ head ) && is_array ($ head )) $ header = array_merge ($ head , $ header );
230+ $ jwt = _jwt_encode ($ header ) . _jwt_encode ($ payload );
231+ return $ jwt . _jwt_encode (jwt_sign ($ jwt , $ secretKey , $ alg ));
232+ }
233+
234+
235+
236+ /** Loads the user session from a cookie
237+ * @param String $cookieName Name of the cookie in which the session is stored (default 'jwt')
238+ * @param String $secretKey Private key to verify integrity of JWT (if null then $_ENV['JWT_SECRET_KEY'])
239+ * @param Int $currentTime UTC timestamp in seconds (optional, can be used for unit tests)
240+ * @return Array containing loaded session values or empty array if no valid session
241+ */
242+ function jwt_session_load ($ cookieName ="jwt " , $ secretKey =null , $ currentTime =null ){
243+ if (!isset ($ _COOKIE [$ cookieName ])) return array ();
244+ return jwt_decode ($ _COOKIE [$ cookieName ]);
245+ }
246+
247+
248+ /** Stores/updates the session in a cookie
249+ * @param Object $jsonObj JSON object containing the custom data that should be stored in the session (if null then $_SESSION will be used)
250+ * @param String $cookieName Name of the cookie in which the session will be stored (default 'jwt')
251+ * @param Int $cookieExpire Expire seconds for how long the session should be valid (0 = until tab/browser gets closed, default '0')
252+ * @param String $cookiePath Path of URL for which the cookie is valid (default '/')
253+ * @param String $cookieDomain Domain for which the session is valid (null for current domain, default null)
254+ * @param Boolean $cookieSecure True if session should only be sent if HTTPs is used (default false)
255+ */
256+ function jwt_session_store ($ jsonObj =null , $ cookieName ="jwt " , $ cookieExpire =0 , $ cookiePath ="/ " , $ cookieDomain =null , $ cookieSecure =false ,
257+ $ cookieHttpOnly =false , $ cookieOptions =array ()){
258+ if (is_null ($ jsonObj )) $ jsonObj = $ _SESSION ;
259+ $ jwt = jwt_encode ($ jsonObj );
260+ if ($ jwt ) setcookie ($ cookieName , $ jwt , $ cookieExpire , $ cookiePath , is_null ($ cookieDomain ) ? '' : $ cookieDomain ,
261+ $ cookieSecure , $ cookieHttpOnly , $ cookieOptions );
262+ }
263+
264+
265+ /** Deletes the session
266+ * @param String $cookieName Name of the cookie in which the session is stored
267+ */
268+ function jwt_session_destroy ($ cookieName ="jwt " ){
269+ unset($ _COOKIE [$ cookieName ]);
270+ setcookie ($ cookieName , null , -1 , '/ ' );
271+ }
272+ ?>
0 commit comments