Completed
Push — master ( a25d19...9be144 )
by Дмитрий
04:12
created

JWT::findKeyByKind()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 9
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 2
crap 12
1
<?php
2
/**
3
 * @author Patsura Dmitry https://github.com/ovr <[email protected]>
4
 */
5
6
namespace SocialConnect\OpenIDConnect;
7
8
use SocialConnect\OpenIDConnect\Exception\InvalidJWT;
9
use SocialConnect\OpenIDConnect\Exception\UnsupportedSignatureAlgoritm;
10
11
class JWT
12
{
13
    /**
14
     * Map of supported algorithms
15
     *
16
     * @var array
17
     */
18
    public static $algorithms = array(
19
        // HS
20
        'HS256' => ['hash_hmac', MHASH_SHA256],
21
        'HS384' => ['hash_hmac', MHASH_SHA384],
22
        'HS512' => ['hash_hmac', MHASH_SHA512],
23
        // RS
24
        'RS256' => ['openssl', OPENSSL_ALGO_SHA256],
25
        'RS384' => ['openssl', OPENSSL_ALGO_SHA384],
26
        'RS512' => ['openssl', OPENSSL_ALGO_SHA512],
27
    );
28
29
    /**
30
     * @var array
31
     */
32
    protected $parts;
33
34
    /**
35
     * @var array
36
     */
37
    protected $header;
38
39
    /**
40
     * @var array
41
     */
42
    protected $payload;
43
44
    /**
45
     * @var string
46
     */
47
    protected $signature;
48
49
    /**
50
     * @param string $input
51
     * @return string
52
     */
53
    public static function urlsafeB64Decode($input)
54
    {
55
        $remainder = strlen($input) % 4;
56
57
        if ($remainder) {
58
            $padlen = 4 - $remainder;
59
            $input .= str_repeat('=', $padlen);
60
        }
61
62
        return base64_decode(strtr($input, '-_', '+/'));
63
    }
64
65
    /**
66
     * @param string $token
67
     * @param array $keys
68
     * @throws InvalidJWT
69
     */
70
    public function __construct($token, array $keys)
71
    {
72
        $parts = explode('.', $token);
73
        if (count($parts) !== 3) {
74
            throw new InvalidJWT('Wrong number of segments');
75
        }
76
77
        list ($header64, $payload64, $token64) = $parts;
78
79
        $headerPayload = base64_decode($header64, true);
80
        if (!$headerPayload) {
81
            throw new InvalidJWT('Cannot decode base64 from header');
82
        }
83
84
        $this->header = json_decode($headerPayload);
0 ignored issues
show
Documentation Bug introduced by
It seems like json_decode($headerPayload) of type * is incompatible with the declared type array of property $header.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
85
        if ($this->header === null) {
86
            throw new InvalidJWT('Cannot decode JSON from header');
87
        }
88
89
        $decodedPayload = base64_decode($payload64, true);
90
        if (!$decodedPayload) {
91
            throw new InvalidJWT('Cannot decode base64 from payload');
92
        }
93
94
        $this->payload = json_decode($decodedPayload);
0 ignored issues
show
Documentation Bug introduced by
It seems like json_decode($decodedPayload) of type * is incompatible with the declared type array of property $payload.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
95
        if ($this->payload === null) {
96
            throw new InvalidJWT('Cannot decode JSON from payload');
97
        }
98
99
        $this->signature = self::urlsafeB64Decode($token64);
100
101
        $this->validate("{$header64}.{$payload64}", $keys);
102
    }
103
104
    /**
105
     * @param string $data
106
     * @param array $keys
107
     * @throws InvalidJWT
108
     */
109
    protected function validate($data, array $keys)
110
    {
111
        if (!isset($this->header->alg)) {
112
            throw new InvalidJWT('No alg inside header');
113
        }
114
115
        if (!isset($this->header->kid)) {
116
            throw new InvalidJWT('No kid inside header');
117
        }
118
119
        $result = $this->verifySignature($data, $keys);
120
        if (!$result) {
121
            throw new InvalidJWT('Unexpected signature');
122
        }
123
    }
124
125
    /**
126
     * @param array $keys
127
     * @param string $kid
128
     * @return JWK
129
     * @throws \RuntimeException
130
     */
131
    protected function findKeyByKind(array $keys, $kid)
132
    {
133
        foreach ($keys as $key) {
134
            if ($key['kid'] === $kid) {
135
                return new JWK($key);
136
            }
137
        }
138
139
        throw new \RuntimeException('Unknown key');
140
    }
141
142
    /**
143
     * @return bool
144
     * @throws \RuntimeException
145
     * @throws \SocialConnect\OpenIDConnect\Exception\UnsupportedSignatureAlgoritm
146
     */
147
    protected function verifySignature($data, array $keys)
148
    {
149
        $supported = isset(self::$algorithms[$this->header->alg]);
150
        if (!$supported) {
151
            throw new UnsupportedSignatureAlgoritm($this->header->alg);
152
        }
153
154
        $jwk = $this->findKeyByKind($keys, $this->header->kid);
155
156
        list ($function, $signatureAlg) = self::$algorithms[$this->header->alg];
157
        switch ($function) {
158
            case 'openssl':
159
                if (!function_exists('openssl_verify')) {
160
                    throw new \RuntimeException('Openssl-ext is required to use RSA encryption.');
161
                }
162
163
                $result = openssl_verify(
164
                    $data,
165
                    $this->signature,
166
                    $jwk->getPublicKey(),
167
                    $signatureAlg
168
                );
169
                
170
                return $result == 1;
171
            case 'hash_hmac':
172
                $hash = hash_hmac($signatureAlg, $data, $jwk->getPublicKey(), true);
173
174
                /**
175
                 * @todo In SocialConnect/Auth 2.0 drop PHP 5.5 support and support for hash_equals emulation
176
                 */
177
                if (function_exists('hash_equals')) {
178
                    return hash_equals($this->signature, $hash);
179
                }
180
181
                if (strlen($this->signature) != strlen($hash)) {
182
                    return false;
183
                }
184
185
                $ret = 0;
186
                $res = $this->signature ^ $hash;
187
188
                for($i = strlen($res) - 1; $i >= 0; $i--) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOR keyword; 0 found
Loading history...
189
                    $ret |= ord($res[$i]);
190
                }
191
192
                return !$ret;
193
        }
194
195
        throw new UnsupportedSignatureAlgoritm($this->header->alg);
196
    }
197
}
198