Completed
Branch master (bda8d5)
by Дмитрий
02:19
created

JWT::decode()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 24.432

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
dl 0
loc 33
rs 9.0111
c 1
b 0
f 0
ccs 4
cts 20
cp 0.2
cc 6
nc 6
nop 2
crap 24.432
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
     * When checking nbf, iat or exp
16
     * we provide additional time screw/leeway
17
     *
18
     * @link https://github.com/SocialConnect/auth/issues/26
19
     */
20
    public static $screw = 0;
21
22
    /**
23
     * Map of supported algorithms
24
     *
25
     * @var array
26
     */
27
    public static $algorithms = array(
28
        // HS
29
        'HS256' => ['hash_hmac', 'SHA256'],
30
        'HS384' => ['hash_hmac', 'SHA384'],
31
        'HS512' => ['hash_hmac', 'SHA512'],
32
        // RS
33
        'RS256' => ['openssl', 'SHA256'],
34
        'RS384' => ['openssl', 'SHA384'],
35
        'RS512' => ['openssl', 'SHA512'],
36
    );
37
38
    /**
39
     * @var array
40
     */
41
    protected $header;
42
43
    /**
44
     * @var array
45
     */
46
    protected $payload;
47
48
    /**
49
     * @var string|null
50
     */
51
    protected $signature;
52
53
    /**
54
     * @param string $input
55
     * @return string
56
     */
57
    public static function urlsafeB64Decode($input)
58
    {
59
        $remainder = strlen($input) % 4;
60
61
        if ($remainder) {
62
            $padlen = 4 - $remainder;
63
            $input .= str_repeat('=', $padlen);
64
        }
65
66
        return base64_decode(strtr($input, '-_', '+/'));
67
    }
68
69
    /**
70
     * @param array $payload
71
     * @param array $header
72
     * @param string|null $signature
73
     */
74 7
    public function __construct(array $payload, array $header, $signature = null)
75
    {
76 7
        $this->payload = $payload;
77 7
        $this->header = $header;
78 7
        $this->signature = $signature;
79 7
    }
80
81
    /**
82
     * @param string $token
83
     * @param array $keys
84
     * @return JWT
85
     * @throws InvalidJWT
86
     */
87 1
    public static function decode($token, array $keys)
88
    {
89 1
        $parts = explode('.', $token);
90 1
        if (count($parts) !== 3) {
91 1
            throw new InvalidJWT('Wrong number of segments');
92
        }
93
94
        list ($header64, $payload64, $signature64) = $parts;
95
96
        $headerPayload = base64_decode($header64);
97
        if (!$headerPayload) {
98
            throw new InvalidJWT('Cannot decode base64 from header');
99
        }
100
101
        $header = json_decode($headerPayload, true);
102
        if ($header === null) {
103
            throw new InvalidJWT('Cannot decode JSON from header');
104
        }
105
106
        $decodedPayload = base64_decode($payload64);
107
        if (!$decodedPayload) {
108
            throw new InvalidJWT('Cannot decode base64 from payload');
109
        }
110
111
        $payload = json_decode($decodedPayload, true);
112
        if ($payload === null) {
113
            throw new InvalidJWT('Cannot decode JSON from payload');
114
        }
115
116
        $token = new self($payload, $header, self::urlsafeB64Decode($signature64));
117
        $token->validate("{$header64}.{$payload64}", $keys);
118
119
        return $token;
120
    }
121
122 3
    protected function validateHeader()
123
    {
124 3
        if (!isset($this->header['alg'])) {
125 1
            throw new InvalidJWT('No alg inside header');
126
        }
127
128 2
        if (!isset($this->header['kid'])) {
129 1
            throw new InvalidJWT('No kid inside header');
130
        }
131 1
    }
132
133 4
    protected function validateClaims()
134
    {
135 4
        $now = time();
136
137
        /**
138
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.5
139
         * "nbf" (Not Before) Claim check
140
         */
141 4
        if (isset($this->payload['nbf']) && $this->payload['nbf'] > ($now + self::$screw)) {
142 1
            throw new InvalidJWT(
143 1
                'nbf (Not Fefore) claim is not valid ' . date(DateTime::RFC3339, $this->payload['nbf'])
144
            );
145
        }
146
147
        /**
148
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.6
149
         * "iat" (Issued At) Claim
150
         */
151 3
        if (isset($this->payload['iat']) && $this->payload['iat'] > ($now + self::$screw)) {
152
            throw new InvalidJWT(
153
                'iat (Issued At) claim is not valid ' . date(DateTime::RFC3339, $this->payload['iat'])
154
            );
155
        }
156
157
        /**
158
         * @link https://tools.ietf.org/html/rfc7519#section-4.1.4
159
         * "exp" (Expiration Time) Claim
160
         */
161 3
        if (isset($this->payload['exp']) && ($now - self::$screw) >= $this->payload['exp']) {
162 1
            throw new InvalidJWT(
163 1
                'exp (Expiration Time) claim is not valid ' . date(DateTime::RFC3339, $this->payload['exp'])
164
            );
165
        }
166 2
    }
167
168
    /**
169
     * @param string $data
170
     * @param array $keys
171
     * @throws InvalidJWT
172
     */
173
    protected function validate($data, array $keys)
174
    {
175
        $this->validateHeader();
176
        $this->validateClaims();
177
178
        $result = $this->verifySignature($data, $keys);
179
        if (!$result) {
180
            throw new InvalidJWT('Unexpected signature');
181
        }
182
    }
183
184
    /**
185
     * @param array $keys
186
     * @param string $kid
187
     * @return JWK
188
     * @throws \RuntimeException
189
     */
190
    protected function findKeyByKind(array $keys, $kid)
191
    {
192
        foreach ($keys as $key) {
193
            if ($key['kid'] === $kid) {
194
                return new JWK($key);
195
            }
196
        }
197
198
        throw new \RuntimeException('Unknown key');
199
    }
200
201
    /**
202
     * @param string $data
203
     * @param array $keys
204
     * @return bool
205
     * @throws UnsupportedSignatureAlgoritm
206
     */
207
    protected function verifySignature($data, array $keys)
208
    {
209
        $supported = isset(self::$algorithms[$this->header['alg']]);
210
        if (!$supported) {
211
            throw new UnsupportedSignatureAlgoritm($this->header['alg']);
212
        }
213
214
        $jwk = $this->findKeyByKind($keys, $this->header['kid']);
215
216
        list ($function, $signatureAlg) = self::$algorithms[$this->header['alg']];
217
        switch ($function) {
218
            case 'openssl':
219
                if (!function_exists('openssl_verify')) {
220
                    throw new \RuntimeException('Openssl-ext is required to use RS encryption.');
221
                }
222
223
                $result = openssl_verify(
224
                    $data,
225
                    $this->signature,
226
                    $jwk->getPublicKey(),
227
                    $signatureAlg
228
                );
229
                
230
                return $result == 1;
231
            case 'hash_hmac':
232
                if (!function_exists('hash_hmac')) {
233
                    throw new \RuntimeException('hash-ext is required to use HS encryption.');
234
                }
235
236
                $hash = hash_hmac($signatureAlg, $data, $jwk->getPublicKey(), true);
237
238
                return hash_equals($this->signature, $hash);
239
        }
240
241
        throw new UnsupportedSignatureAlgoritm($this->header['alg']);
242
    }
243
}
244