Skip to content

Commit ceb5996

Browse files
committed
Added jwt-session.php, but not tested yet
1 parent 8b0e21a commit ceb5996

2 files changed

Lines changed: 277 additions & 1 deletion

File tree

.env

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
DB_HOST=localhost
33
DB_NAME=myDB
44
DB_USER=db_user
5-
DB_PASS=db_pass
5+
DB_PASS=db_pass
6+
7+
# JWT
8+
JWT_SECRET_KEY=
9+
JWT_LEEWAY_SEC=60

scripts/jwt-session.php

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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

Comments
 (0)