Completed
Pull Request — master (#7)
by John
02:22
created

JwtKey::validateClaims()   D

Complexity

Conditions 21
Paths 21

Size

Total Lines 36
Code Lines 23

Duplication

Lines 15
Ratio 41.67 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 15
loc 36
rs 4.8505
cc 21
eloc 23
nc 21
nop 1

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
 * This file is part of the KleijnWeb\JwtBundle package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace KleijnWeb\JwtBundle\Authenticator;
10
11
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\SignatureValidator;
12
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\HmacValidator;
13
use KleijnWeb\JwtBundle\Authenticator\SignatureValidator\RsaValidator;
14
15
/**
16
 * @author John Kleijn <[email protected]>
17
 */
18
class JwtKey
19
{
20
    const TYPE_HMAC = 'HS256';
21
    const TYPE_RSA = 'RS256';
22
23
    /**
24
     * @var string
25
     */
26
    private $id;
27
28
    /**
29
     * @var string
30
     */
31
    private $issuer;
32
33
    /**
34
     * @var string
35
     */
36
    private $type = self::TYPE_HMAC;
37
38
    /**
39
     * @var string
40
     */
41
    private $audience;
42
43
    /**
44
     * @var int
45
     */
46
    private $minIssueTime;
47
48
    /**
49
     * @var array
50
     */
51
    private $requiredClaims = [];
52
53
    /**
54
     * @var int
55
     */
56
    private $issuerTimeLeeway;
57
58
    /**
59
     * @var string
60
     */
61
    private $secret;
62
63
    /**
64
     * @var SecretLoader
65
     */
66
    private $secretLoader;
67
68
    /**
69
     * @param array $options
70
     */
71
    public function __construct(array $options)
72
    {
73
        if (!isset($options['secret']) && !isset($options['loader'])) {
74
            throw new \InvalidArgumentException("Need a secret or a loader to verify tokens");
75
        }
76
        if (isset($options['secret']) && isset($options['loader'])) {
77
            throw new \InvalidArgumentException("Cannot configure both secret and loader");
78
        }
79
        $defaults = [
80
            'kid'          => null,
81
            'issuer'       => null,
82
            'audience'     => null,
83
            'minIssueTime' => null,
84
            'leeway'       => 0,
85
            'type'         => $this->type,
86
            'require'      => $this->requiredClaims,
87
        ];
88
89
        $options                = array_merge($defaults, $options);
90
        $this->issuer           = $options['issuer'];
91
        $this->audience         = $options['audience'];
92
        $this->type             = $options['type'];
93
        $this->minIssueTime     = $options['minIssueTime'];
94
        $this->requiredClaims   = $options['require'];
95
        $this->issuerTimeLeeway = $options['leeway'];
96
        $this->id               = $options['kid'];
97
        $this->secret           = isset($options['secret']) ? $options['secret'] : null;
98
        $this->secretLoader     = isset($options['loader']) ? $options['loader'] : null;
99
    }
100
101
    /**
102
     * @return string
103
     */
104
    public function getId()
105
    {
106
        return $this->id;
107
    }
108
109
    /**
110
     * @param JwtToken $token
111
     *
112
     * @throws \InvalidArgumentException
113
     */
114
    public function validateToken(JwtToken $token)
115
    {
116
        $this->validateHeader($token->getHeader());
117
        $this->validateClaims($token->getClaims());
118
119
        if (!$this->secretLoader) {
120
            $token->validateSignature($this->secret, $this->getSignatureValidator());
121
            return;
122
        }
123
        $token->validateSignature($this->secretLoader->load($token), $this->getSignatureValidator());
124
    }
125
126
    /**
127
     * @param array $header
128
     *
129
     * @throws \InvalidArgumentException
130
     */
131
    public function validateHeader(array $header)
132
    {
133
        if (!isset($header['alg'])) {
134
            throw new \InvalidArgumentException("Missing 'alg' in header");
135
        }
136
        if (!isset($header['typ'])) {
137
            throw new \InvalidArgumentException("Missing 'typ' in header");
138
        }
139
        if ($this->type !== $header['alg']) {
140
            throw new \InvalidArgumentException("Algorithm mismatch");
141
        }
142
    }
143
144
    /**
145
     * @param array $claims
146
     *
147
     * @throws \InvalidArgumentException
148
     */
149
    public function validateClaims(array $claims)
150
    {
151
        if ($this->requiredClaims) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->requiredClaims of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
152
            $missing = array_diff_key(array_flip($this->requiredClaims), $claims);
153
            if (count($missing)) {
154
                throw new \InvalidArgumentException("Missing claims: " . implode(', ', $missing));
155
            }
156
        }
157
        if ($this->issuer && !isset($claims['iss'])) {
158
            throw new \InvalidArgumentException("Claim 'iss' is required");
159
        }
160
        if ($this->minIssueTime && !isset($claims['iat'])) {
161
            throw new \InvalidArgumentException("Claim 'iat' is required");
162
        }
163
        if ($this->audience && !isset($claims['aud'])) {
164
            throw new \InvalidArgumentException("Claim 'aud' is required");
165
        }
166
        if (!isset($claims['prn']) || empty($claims['prn'])) {
167
            throw new \InvalidArgumentException("Missing principle claim");
168
        }
169 View Code Duplication
        if (isset($claims['exp']) && $claims['exp'] < time()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
170
            throw new \InvalidArgumentException("Token is expired by 'exp'");
171
        }
172 View Code Duplication
        if (isset($claims['iat']) && $claims['iat'] < $this->minIssueTime) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
173
            throw new \InvalidArgumentException("Server deemed your token too old");
174
        }
175 View Code Duplication
        if (isset($claims['nbf']) && $claims['nbf'] > time()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
176
            throw new \InvalidArgumentException("Token not valid yet");
177
        }
178 View Code Duplication
        if (isset($claims['iss']) && $claims['iss'] !== $this->issuer) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
179
            throw new \InvalidArgumentException("Issuer mismatch");
180
        }
181 View Code Duplication
        if (isset($claims['aud']) && $claims['aud'] !== $this->audience) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
182
            throw new \InvalidArgumentException("Audience mismatch");
183
        }
184
    }
185
186
187
    /**
188
     * @return SignatureValidator
189
     */
190
    public function getSignatureValidator()
191
    {
192
        if ($this->type == self::TYPE_RSA) {
193
            return new RsaValidator();
194
        }
195
196
        return new HmacValidator();
197
    }
198
199
    /**
200
     * Prevent accidental persistence of secret
201
     */
202
    final public function __sleep()
203
    {
204
        return [];
205
    }
206
}
207