Completed
Push — master ( 9be144...167f91 )
by Дмитрий
07:36
created

JWT::validate()   C

Complexity

Conditions 10
Paths 7

Size

Total Lines 47
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 0
cts 29
cp 0
rs 5.1578
c 0
b 0
f 0
cc 10
eloc 18
nc 7
nop 2
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Patsura Dmitry https://github.com/ovr <[email protected]>
4
 */
5
6
namespace SocialConnect\OpenIDConnect;
7
8
use DateTime;
9
use SocialConnect\OpenIDConnect\Exception\InvalidJWT;
10
use SocialConnect\OpenIDConnect\Exception\UnsupportedSignatureAlgoritm;
11
12
class JWT
13
{
14
    /**
15
     * Map of supported algorithms
16
     *
17
     * @var array
18
     */
19
    public static $algorithms = array(
20
        // HS
21
        'HS256' => ['hash_hmac', MHASH_SHA256],
22
        'HS384' => ['hash_hmac', MHASH_SHA384],
23
        'HS512' => ['hash_hmac', MHASH_SHA512],
24
        // RS
25
        'RS256' => ['openssl', OPENSSL_ALGO_SHA256],
26
        'RS384' => ['openssl', OPENSSL_ALGO_SHA384],
27
        'RS512' => ['openssl', OPENSSL_ALGO_SHA512],
28
    );
29
30
    /**
31
     * @var array
32
     */
33
    protected $parts;
34
35
    /**
36
     * @var array
37
     */
38
    protected $header;
39
40
    /**
41
     * @var array
42
     */
43
    protected $payload;
44
45
    /**
46
     * @var string
47
     */
48
    protected $signature;
49
50
    /**
51
     * @param string $input
52
     * @return string
53
     */
54
    public static function urlsafeB64Decode($input)
55
    {
56
        $remainder = strlen($input) % 4;
57
58
        if ($remainder) {
59
            $padlen = 4 - $remainder;
60
            $input .= str_repeat('=', $padlen);
61
        }
62
63
        return base64_decode(strtr($input, '-_', '+/'));
64
    }
65
66
    /**
67
     * @param string $token
68
     * @param array $keys
69
     * @throws InvalidJWT
70
     */
71
    public function __construct($token, array $keys)
72
    {
73
        $parts = explode('.', $token);
74
        if (count($parts) !== 3) {
75
            throw new InvalidJWT('Wrong number of segments');
76
        }
77
78
        list ($header64, $payload64, $token64) = $parts;
79
80
        $headerPayload = base64_decode($header64, true);
81
        if (!$headerPayload) {
82
            throw new InvalidJWT('Cannot decode base64 from header');
83
        }
84
85
        $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...
86
        if ($this->header === null) {
87
            throw new InvalidJWT('Cannot decode JSON from header');
88
        }
89
90
        $decodedPayload = base64_decode($payload64, true);
91
        if (!$decodedPayload) {
92
            throw new InvalidJWT('Cannot decode base64 from payload');
93
        }
94
95
        $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...
96
        if ($this->payload === null) {
97
            throw new InvalidJWT('Cannot decode JSON from payload');
98
        }
99
100
        $this->signature = self::urlsafeB64Decode($token64);
101
102
        $this->validate("{$header64}.{$payload64}", $keys);
103
    }
104
105
    /**
106
     * @param string $data
107
     * @param array $keys
108
     * @throws InvalidJWT
109
     */
110
    protected function validate($data, array $keys)
0 ignored issues
show
Complexity introduced by
This operation has 216 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
111
    {
112
        $now = time();
113
114
        if (!isset($this->header->alg)) {
115
            throw new InvalidJWT('No alg inside header');
116
        }
117
118
        if (!isset($this->header->kid)) {
119
            throw new InvalidJWT('No kid inside header');
120
        }
121
122
        $result = $this->verifySignature($data, $keys);
123
        if (!$result) {
124
            throw new InvalidJWT('Unexpected signature');
125
        }
126
127
        /**
128
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.5
129
         * "nbf" (Not Before) Claim check
130
         */
131
        if (isset($this->payload->nbf) && $this->payload->nbf > ($now)) {
132
            throw new InvalidJWT(
133
                'nbf (Not Fefore) claim is not valid ' . date(DateTime::RFC3339, $this->payload->nbf)
134
            );
135
        }
136
137
        /**
138
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.6
139
         * "iat" (Issued At) Claim
140
         */
141
        if (isset($this->payload->iat) && $this->payload->iat > $now) {
142
            throw new InvalidJWT(
143
                'iat (Issued At) claim is not valid ' . date(DateTime::RFC3339, $this->payload->ait)
144
            );
145
        }
146
147
        /**
148
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.4
149
         * "exp" (Expiration Time) Claim
150
         */
151
        if (isset($this->payload->exp) && $now >= $this->payload->exp) {
152
            throw new InvalidJWT(
153
                'exp (Expiration Time) claim is not valid ' . date(DateTime::RFC3339, $this->payload->ait)
154
            );
155
        }
156
    }
157
158
    /**
159
     * @param array $keys
160
     * @param string $kid
161
     * @return JWK
162
     * @throws \RuntimeException
163
     */
164
    protected function findKeyByKind(array $keys, $kid)
165
    {
166
        foreach ($keys as $key) {
167
            if ($key['kid'] === $kid) {
168
                return new JWK($key);
169
            }
170
        }
171
172
        throw new \RuntimeException('Unknown key');
173
    }
174
175
    /**
176
     * @return bool
177
     * @throws \RuntimeException
178
     * @throws \SocialConnect\OpenIDConnect\Exception\UnsupportedSignatureAlgoritm
179
     */
180
    protected function verifySignature($data, array $keys)
181
    {
182
        $supported = isset(self::$algorithms[$this->header->alg]);
183
        if (!$supported) {
184
            throw new UnsupportedSignatureAlgoritm($this->header->alg);
185
        }
186
187
        $jwk = $this->findKeyByKind($keys, $this->header->kid);
188
189
        list ($function, $signatureAlg) = self::$algorithms[$this->header->alg];
190
        switch ($function) {
191
            case 'openssl':
192
                if (!function_exists('openssl_verify')) {
193
                    throw new \RuntimeException('Openssl-ext is required to use RSA encryption.');
194
                }
195
196
                $result = openssl_verify(
197
                    $data,
198
                    $this->signature,
199
                    $jwk->getPublicKey(),
200
                    $signatureAlg
201
                );
202
                
203
                return $result == 1;
204
            case 'hash_hmac':
205
                $hash = hash_hmac($signatureAlg, $data, $jwk->getPublicKey(), true);
206
207
                /**
208
                 * @todo In SocialConnect/Auth 2.0 drop PHP 5.5 support and support for hash_equals emulation
209
                 */
210
                if (function_exists('hash_equals')) {
211
                    return hash_equals($this->signature, $hash);
212
                }
213
214
                if (strlen($this->signature) != strlen($hash)) {
215
                    return false;
216
                }
217
218
                $ret = 0;
219
                $res = $this->signature ^ $hash;
220
221
                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...
222
                    $ret |= ord($res[$i]);
223
                }
224
225
                return !$ret;
226
        }
227
228
        throw new UnsupportedSignatureAlgoritm($this->header->alg);
229
    }
230
}
231