Rev 2 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
2 | daniel-mar | 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($signature, $hash)) 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 | } |