├── README.md ├── app_client.php ├── index.html └── jwt.php /README.md: -------------------------------------------------------------------------------- 1 | # jwtphpjquery 2 | A simple example that implements JSON Web Tokens using PHP and JQuery. 3 | 4 | Visit my blog article on Medium for an explanation of these sample files. 5 | https://medium.com/@crmcmullen/simple-example-using-json-web-tokens-with-php-and-jquery-c648a80854c 6 | -------------------------------------------------------------------------------- /app_client.php: -------------------------------------------------------------------------------- 1 | $token); 54 | $jsonEncodedReturnArray = json_encode($returnArray, JSON_PRETTY_PRINT); 55 | echo $jsonEncodedReturnArray; 56 | 57 | } 58 | else { 59 | $returnArray = array('error' => 'Invalid user ID or password.'); 60 | $jsonEncodedReturnArray = json_encode($returnArray, JSON_PRETTY_PRINT); 61 | echo $jsonEncodedReturnArray; 62 | } 63 | 64 | break; 65 | 66 | case 'GET': 67 | 68 | $token = null; 69 | 70 | if (isset($_GET['token'])) {$token = $_GET['token'];} 71 | 72 | if (!is_null($token)) { 73 | 74 | require_once('jwt.php'); 75 | 76 | // Get our server-side secret key from a secure location. 77 | $serverKey = '5f2b5cdbe5194f10b3241568fe4e2b24'; 78 | 79 | try { 80 | $payload = JWT::decode($token, $serverKey, array('HS256')); 81 | $returnArray = array('userId' => $payload->userId); 82 | if (isset($payload->exp)) { 83 | $returnArray['exp'] = date(DateTime::ISO8601, $payload->exp);; 84 | } 85 | } 86 | catch(Exception $e) { 87 | $returnArray = array('error' => $e->getMessage()); 88 | } 89 | } 90 | else { 91 | $returnArray = array('error' => 'You are not logged in with a valid token.'); 92 | } 93 | 94 | // return to caller 95 | $jsonEncodedReturnArray = json_encode($returnArray, JSON_PRETTY_PRINT); 96 | echo $jsonEncodedReturnArray; 97 | 98 | break; 99 | 100 | default: 101 | $returnArray = array('error' => 'You have requested an invalid method.'); 102 | $jsonEncodedReturnArray = json_encode($returnArray, JSON_PRETTY_PRINT); 103 | echo $jsonEncodedReturnArray; 104 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JWT Example 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /jwt.php: -------------------------------------------------------------------------------- 1 | array('hash_hmac', 'SHA256'), 19 | 'HS512' => array('hash_hmac', 'SHA512'), 20 | 'HS384' => array('hash_hmac', 'SHA384'), 21 | 'RS256' => array('openssl', 'SHA256'), 22 | 'RS384' => array('openssl', 'SHA384'), 23 | 'RS512' => array('openssl', 'SHA512'), 24 | ); 25 | /** ---------------------------------------------------------------------------------------------------------- 26 | * Decodes a JWT string into a PHP object. 27 | * 28 | * @param string $token The JSON web token 29 | * @param string|array $key The secret key 30 | * @param array $allowed_algs If the algorithm used is asymmetric, this is the public key list 31 | * of supported verification algorithms. Supported algorithms are: 32 | * 'HS256', 'HS384', 'HS512' and 'RS256' 33 | * 34 | * @return object The JWT's payload as a PHP object 35 | * 36 | */ 37 | public static function decode($token, $key, array $allowed_algs = array()) 38 | { 39 | if ((!isset($timestamp)) || (is_null($timestamp))) { 40 | $timestamp = time(); 41 | } 42 | 43 | if (empty($key)) { 44 | throw new Exception('Invalid or missing key.'); 45 | } 46 | 47 | $tokenSegments = explode('.', $token); 48 | 49 | if (count($tokenSegments) != 3) { 50 | throw new Exception('Wrong number of segments'); 51 | } 52 | 53 | list($headb64, $bodyb64, $cryptob64) = $tokenSegments; 54 | if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { 55 | throw new Exception('Invalid header encoding'); 56 | } 57 | 58 | if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { 59 | throw new Exception('Invalid claims encoding'); 60 | } 61 | 62 | if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { 63 | throw new Exception('Invalid signature encoding'); 64 | } 65 | 66 | if (empty($header->alg)) { 67 | throw new Exception('Empty algorithm'); 68 | } 69 | 70 | if (empty(static::$supported_algs[$header->alg])) { 71 | throw new Exception('Algorithm not supported'); 72 | } 73 | 74 | if (!in_array($header->alg, $allowed_algs)) { 75 | throw new Exception('Algorithm not allowed'); 76 | } 77 | 78 | if (is_array($key) || $key instanceof ArrayAccess) { 79 | if (isset($header->kid)) { 80 | if (!isset($key[$header->kid])) { 81 | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); 82 | } 83 | $key = $key[$header->kid]; 84 | } else { 85 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); 86 | } 87 | } 88 | 89 | // Check the signature 90 | if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { 91 | throw new Exception('Signature verification failed'); 92 | } 93 | 94 | // Check if the nbf if it is defined. This is the time that the 95 | // token can actually be used. If it's not yet that time, abort. 96 | if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { 97 | throw new Exception( 98 | 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) 99 | ); 100 | } 101 | 102 | // Check that this token has been created before 'now'. This prevents 103 | // using tokens that have been created for later use (and haven't 104 | // correctly used the nbf claim). 105 | if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { 106 | throw new Exception( 107 | 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) 108 | ); 109 | } 110 | 111 | // Check if this token has expired. 112 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { 113 | throw new Exception('Expired token'); 114 | } 115 | 116 | return $payload; 117 | } 118 | /** ---------------------------------------------------------------------------------------------------------- 119 | * Converts and signs a PHP object or array into a JWT string. 120 | * 121 | * @param object|array $payload PHP object or array 122 | * @param string $key The secret key. 123 | * If the algorithm used is asymmetric, this is the private key 124 | * @param string $alg The signing algorithm. 125 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 126 | * @param mixed $keyId 127 | * @param array $head An array with header elements to attach 128 | * 129 | * @return string A signed JWT 130 | * 131 | */ 132 | public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) 133 | { 134 | $header = array('typ' => 'JWT', 'alg' => $alg); 135 | 136 | if ($keyId !== null) { 137 | $header['kid'] = $keyId; 138 | } 139 | 140 | if ( isset($head) && is_array($head) ) { 141 | $header = array_merge($head, $header); 142 | } 143 | 144 | $segments = array(); 145 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); 146 | $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); 147 | $signing_input = implode('.', $segments); 148 | $signature = static::sign($signing_input, $key, $alg); 149 | $segments[] = static::urlsafeB64Encode($signature); 150 | 151 | return implode('.', $segments); 152 | } 153 | /** ---------------------------------------------------------------------------------------------------------- 154 | * Sign a string with a given key and algorithm. 155 | * 156 | * @param string $msg The message to sign 157 | * @param string|resource $key The secret key 158 | * @param string $alg The signing algorithm. 159 | * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' 160 | * 161 | * @return string An encrypted message 162 | * 163 | */ 164 | public static function sign($msg, $key, $alg = 'HS256') 165 | { 166 | if (empty(static::$supported_algs[$alg])) { 167 | throw new Exception('Algorithm not supported'); 168 | } 169 | list($function, $algorithm) = static::$supported_algs[$alg]; 170 | switch($function) { 171 | case 'hash_hmac': 172 | return hash_hmac($algorithm, $msg, $key, true); 173 | case 'openssl': 174 | $signature = ''; 175 | $success = openssl_sign($msg, $signature, $key, $algorithm); 176 | if (!$success) { 177 | throw new Exception("OpenSSL unable to sign data"); 178 | } else { 179 | return $signature; 180 | } 181 | } 182 | } 183 | /** ---------------------------------------------------------------------------------------------------------- 184 | * Verify a signature with the message, key and method. Not all methods 185 | * are symmetric, so we must have a separate verify and sign method. 186 | * 187 | * @param string $msg The original message (header and body) 188 | * @param string $signature The original signature 189 | * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key 190 | * @param string $alg The algorithm 191 | * 192 | * @return bool 193 | */ 194 | private static function verify($msg, $signature, $key, $alg) 195 | { 196 | if (empty(static::$supported_algs[$alg])) { 197 | throw new Exception('Algorithm not supported'); 198 | } 199 | list($function, $algorithm) = static::$supported_algs[$alg]; 200 | switch($function) { 201 | case 'openssl': 202 | $success = openssl_verify($msg, $signature, $key, $algorithm); 203 | if ($success === 1) { 204 | return true; 205 | } elseif ($success === 0) { 206 | return false; 207 | } 208 | // returns 1 on success, 0 on failure, -1 on error. 209 | throw new Exception( 210 | 'OpenSSL error: ' . openssl_error_string() 211 | ); 212 | case 'hash_hmac': 213 | default: 214 | $hash = hash_hmac($algorithm, $msg, $key, true); 215 | if (function_exists('hash_equals')) { 216 | return hash_equals($signature, $hash); 217 | } 218 | $len = min(static::safeStrlen($signature), static::safeStrlen($hash)); 219 | $status = 0; 220 | for ($i = 0; $i < $len; $i++) { 221 | $status |= (ord($signature[$i]) ^ ord($hash[$i])); 222 | } 223 | $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); 224 | return ($status === 0); 225 | } 226 | } 227 | /** ---------------------------------------------------------------------------------------------------------- 228 | * Decode a JSON string into a PHP object. 229 | * 230 | * @param string $input JSON string 231 | * 232 | * @return object Object representation of JSON string 233 | * 234 | * @throws Exception Provided string was invalid JSON 235 | */ 236 | public static function jsonDecode($input) 237 | { 238 | if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { 239 | /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you 240 | * to specify that large ints (like Steam Transaction IDs) should be treated as 241 | * strings, rather than the PHP default behaviour of converting them to floats. 242 | */ 243 | $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); 244 | } else { 245 | /** Not all servers will support that, however, so for older versions we must 246 | * manually detect large ints in the JSON string and quote them (thus converting 247 | *them to strings) before decoding, hence the preg_replace() call. 248 | */ 249 | $max_int_length = strlen((string) PHP_INT_MAX) - 1; 250 | $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); 251 | $obj = json_decode($json_without_bigints); 252 | } 253 | if (function_exists('json_last_error') && $errno = json_last_error()) { 254 | static::handleJsonError($errno); 255 | } elseif ($obj === null && $input !== 'null') { 256 | throw new Exception('Null result with non-null input'); 257 | } 258 | return $obj; 259 | } 260 | /** ---------------------------------------------------------------------------------------------------------- 261 | * Encode a PHP object into a JSON string. 262 | * 263 | * @param object|array $input A PHP object or array 264 | * 265 | * @return string JSON representation of the PHP object or array 266 | * 267 | * @throws Exception Provided object could not be encoded to valid JSON 268 | */ 269 | public static function jsonEncode($input) 270 | { 271 | $json = json_encode($input); 272 | if (function_exists('json_last_error') && $errno = json_last_error()) { 273 | static::handleJsonError($errno); 274 | } elseif ($json === 'null' && $input !== null) { 275 | throw new Exception('Null result with non-null input'); 276 | } 277 | return $json; 278 | } 279 | /** ---------------------------------------------------------------------------------------------------------- 280 | * Decode a string with URL-safe Base64. 281 | * 282 | * @param string $input A Base64 encoded string 283 | * 284 | * @return string A decoded string 285 | */ 286 | public static function urlsafeB64Decode($input) 287 | { 288 | $remainder = strlen($input) % 4; 289 | if ($remainder) { 290 | $padlen = 4 - $remainder; 291 | $input .= str_repeat('=', $padlen); 292 | } 293 | return base64_decode(strtr($input, '-_', '+/')); 294 | } 295 | /** ---------------------------------------------------------------------------------------------------------- 296 | * Encode a string with URL-safe Base64. 297 | * 298 | * @param string $input The string you want encoded 299 | * 300 | * @return string The base64 encode of what you passed in 301 | */ 302 | public static function urlsafeB64Encode($input) 303 | { 304 | return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); 305 | } 306 | /** ---------------------------------------------------------------------------------------------------------- 307 | * Helper method to create a JSON error. 308 | * 309 | * @param int $errno An error number from json_last_error() 310 | * 311 | * @return void 312 | */ 313 | private static function handleJsonError($errno) 314 | { 315 | $messages = array( 316 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 317 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', 318 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 319 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 320 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 321 | ); 322 | throw new Exception( 323 | isset($messages[$errno]) 324 | ? $messages[$errno] 325 | : 'Unknown JSON error: ' . $errno 326 | ); 327 | } 328 | /** ---------------------------------------------------------------------------------------------------------- 329 | * Get the number of bytes in cryptographic strings. 330 | * 331 | * @param string 332 | * 333 | * @return int 334 | */ 335 | private static function safeStrlen($str) 336 | { 337 | if (function_exists('mb_strlen')) { 338 | return mb_strlen($str, '8bit'); 339 | } 340 | return strlen($str); 341 | } 342 | } --------------------------------------------------------------------------------