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