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