Subversion Repositories oidplus

Rev

Rev 604 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 604 Rev 1469
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