Subversion Repositories php_utils

Rev

Rev 5 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * JWT Decoder for PHP
  5.  * Copyright 2021 Daniel Marschall, ViaThinkSoft
  6.  * Version 2021-05-15
  7.  *
  8.  * Licensed under the Apache License, Version 2.0 (the "License");
  9.  * you may not use this file except in compliance with the License.
  10.  * You may obtain a copy of the License at
  11.  *
  12.  *     http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing, software
  15.  * distributed under the License is distributed on an "AS IS" BASIS,
  16.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17.  * See the License for the specific language governing permissions and
  18.  * limitations under the License.
  19.  */
  20.  
  21. function decode_idtoken($id_token, $verification_certs=null, $allowed_algorithms = array()) {
  22.         // Parts taken and simplified from https://github.com/firebase/php-jwt , licensed by BSD-3-clause
  23.         // Here is a great page for encode and decode tokens for testing: https://jwt.io/
  24.  
  25.         $parts = explode('.', $id_token);
  26.         if (count($parts) === 5) return false; // encrypted JWT not yet supported
  27.         if (count($parts) !== 3) return false;
  28.         list($header_base64, $payload_base64, $signature_base64) = $parts;
  29.  
  30.         $header_ary = json_decode(urlsafeB64Decode($header_base64),true);
  31.         if ($header_ary['typ'] !== 'JWT') return false;
  32.  
  33.         if ($verification_certs) {
  34.                 $key = isset($header_ary['kid']) ? $verification_certs[$header_ary['kid']] : $verification_certs;
  35.  
  36.                 $msg = $header_base64.'.'.$payload_base64;
  37.                 $signature = urlsafeB64Decode($signature_base64);
  38.  
  39.                 $jwt_algo = $header_ary['alg'];
  40.  
  41.                 // see https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
  42.                 //     https://datatracker.ietf.org/doc/html/rfc8725#section-3.1
  43.                 if (!in_array($jwt_algo, $allowed_algorithms)) return false;
  44.  
  45.                 if ($jwt_algo != 'none') {
  46.                         $php_algo = 'SHA'.substr($jwt_algo,2,3);
  47.                         switch (substr($jwt_algo,0,2)) {
  48.                                 case 'ES':
  49.                                         // OpenSSL expects an ASN.1 DER sequence for ES256 signatures
  50.                                         $signature = signatureToDER($signature);
  51.                                         if (!function_exists('openssl_verify')) break; // if OpenSSL is not installed, we just accept the JWT
  52.                                         if (!openssl_verify($msg, $signature, $key, $php_algo)) return false;
  53.                                         break;
  54.                                 case 'RS':
  55.                                         if (!function_exists('openssl_verify')) break; // if OpenSSL is not installed, we just accept the JWT
  56.                                         if (!openssl_verify($msg, $signature, $key, $php_algo)) return false;
  57.                                         break;
  58.                                 case 'HS':
  59.                                         $hash = @hash_hmac($php_algo, $msg, $key, true);
  60.                                         if (!$hash) break; // if the hash algo is not available, we just accept the JWT
  61.                                         if (!hash_equals($hash, $signature)) return false;
  62.                                         break;
  63.                                 case 'PS':
  64.                                         // This feature is new and not yet available in php-jwt
  65.                                         file_put_contents($msg_file = tempnam("/tmp", ""), $msg);
  66.                                         file_put_contents($sig_file = tempnam("/tmp", ""), $signature);
  67.                                         file_put_contents($key_file = tempnam("/tmp", ""), $key);
  68.                                         $ec = -1;
  69.                                         $out = array();
  70.                                         $cmd = "openssl dgst -".strtolower($php_algo)." -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 -verify ".escapeshellarg($key_file)." -signature ".escapeshellarg($sig_file)." ".escapeshellarg($msg_file);
  71.                                         $cmd .= (strtoupper(substr(PHP_OS,0,3)) === 'WIN') ? ' 2> NUL' : ' 2> /dev/null';
  72.                                         exec($cmd, $out, $ec);
  73.                                         unlink($msg_file);
  74.                                         unlink($sig_file);
  75.                                         unlink($key_file);
  76.                                         if (($ec !== 0) && (count($out) === 0)) break; // If OpenSSL is not found, we just accept the JWT
  77.                                         if (($ec !== 0) || (strpos(implode("\n",$out),"Verified OK") === false)) return false;
  78.                                         break;
  79.                                 default:
  80.                                         return false;
  81.                         }
  82.                 }
  83.         }
  84.  
  85.         $payload_ary = json_decode(urlsafeB64Decode($payload_base64), true);
  86.  
  87.         $leeway = 60; // 1 Minute
  88.         if (isset($payload_ary['nbf']) && (time()+$leeway<$payload_ary['nbf'])) return false;
  89.         if (isset($payload_ary['exp']) && (time()-$leeway>$payload_ary['exp'])) return false;
  90.  
  91.         return $payload_ary;
  92. }
  93.  
  94. function urlsafeB64Decode($input) {
  95.         // Taken from https://github.com/firebase/php-jwt , licensed by BSD-3-clause
  96.         $remainder = strlen($input) % 4;
  97.         if ($remainder) {
  98.                 $padlen = 4 - $remainder;
  99.                 $input .= str_repeat('=', $padlen);
  100.         }
  101.         return base64_decode(strtr($input, '-_', '+/'));
  102. }
  103.  
  104. function signatureToDER($sig) {
  105.         // Taken from https://github.com/firebase/php-jwt , licensed by BSD-3-clause, modified
  106.  
  107.         // Separate the signature into r-value and s-value
  108.         list($r, $s) = str_split($sig, (int) (strlen($sig) / 2));
  109.  
  110.         // Trim leading zeros
  111.         $r = ltrim($r, "\x00");
  112.         $s = ltrim($s, "\x00");
  113.  
  114.         // Convert r-value and s-value from unsigned big-endian integers to signed two's complement
  115.         if (ord($r[0]) > 0x7f) $r = "\x00" . $r;
  116.         if (ord($s[0]) > 0x7f) $s = "\x00" . $s;
  117.  
  118.         $der_r = chr(0x00/*primitive*/ | 0x02/*INTEGER*/).chr(strlen($r)).$r;
  119.         $der_s = chr(0x00/*primitive*/ | 0x02/*INTEGER*/).chr(strlen($s)).$s;
  120.         $der = chr(0x20/*constructed*/ | 0x10/*SEQUENCE*/).chr(strlen($der_r.$der_s)).$der_r.$der_s;
  121.         return $der;
  122. }
  123.