User::assertClaims()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 11
cts 11
cp 1
rs 9.7666
c 0
b 0
f 0
cc 4
nc 8
nop 1
crap 4
1
<?php
2
/**
3
 * JWT powered User for Yii 2
4
 *
5
 * @see       https://github.com/sergeymakinen/yii2-jwt-user
6
 * @copyright Copyright (c) 2016-2017 Sergey Makinen (https://makinen.ru)
7
 * @license   https://github.com/sergeymakinen/yii2-jwt-user/blob/master/LICENSE MIT License
8
 */
9
10
namespace sergeymakinen\yii\jwtuser;
11
12
use Lcobucci\JWT\Builder;
13
use Lcobucci\JWT\Parser;
14
use Lcobucci\JWT\Signer\Hmac\Sha256;
15
use Lcobucci\JWT\Token;
16
use Lcobucci\JWT\ValidationData;
17
use yii\base\InvalidValueException;
18
use yii\web\Cookie;
19
use yii\web\IdentityInterface;
20
21
/**
22
 * User class with a JWT cookie as a backend.
23
 *
24
 * @see https://jwt.io
25
 * @see https://tools.ietf.org/html/rfc7519
26
 * @see \yii\web\User
27
 */
28
class User extends \yii\web\User
29
{
30
    /**
31
     * @var string JWT sign key. Must be random and secret.
32
     * @see https://tools.ietf.org/html/rfc7519#section-11
33
     * @since 3.0
34
     */
35
    public $key;
36
37
    /**
38
     * @var bool whether to use a [[IdentityInterface::getAuthKey()]] value to validate a token.
39
     * @since 3.0
40
     */
41
    public $useAuthKey = true;
42
43
    /**
44
     * @var bool whether to append a [[IdentityInterface::getAuthKey()]] value to the sign key or store it as a claim.
45
     * @since 3.0
46
     */
47
    public $appendAuthKey = false;
48
49
    /**
50
     * @var \Closure|string JWT audience claim ("aud").
51
     * @see https://tools.ietf.org/html/rfc7519#section-4.1.3
52
     * @since 1.1
53
     */
54
    public $audience;
55
56
    /**
57
     * @var \Closure|string JWT issuer claim ("iss").
58
     * @see https://tools.ietf.org/html/rfc7519#section-4.1.1
59
     * @since 3.0
60
     */
61
    public $issuer;
62
63
    /**
64
     * @inheritDoc
65
     */
66 432
    protected function renewIdentityCookie()
67
    {
68
        try {
69
            /** @var IdentityInterface $identity */
70
            /** @var Token $token */
71 432
            list($identity, $token) = $this->getIdentityAndTokenFromCookie();
72 216
            if ($identity === null) {
73 216
                return;
74
            }
75 216
        } catch (\Exception $e) {
76 216
            if ($e instanceof InvalidValueException) {
77
                throw $e;
78
            }
79
80 216
            return;
81
        }
82 216
        $now = time();
83 216
        $builder = $this->createBuilderFromToken($token)
84 216
            ->setNotBefore($now);
85 216
        if ($token->hasClaim('exp')) {
86 162
            $builder->setExpiration($now + ($token->getClaim('exp') - $token->getClaim('nbf')));
87
        }
88 216
        $this->sendToken($builder, $identity);
89 216
    }
90
91
    /**
92
     * @inheritDoc
93
     */
94 432
    protected function sendIdentityCookie($identity, $duration)
95
    {
96 432
        $now = time();
97 432
        $builder = (new Builder())
98 432
            ->setIssuedAt($now)
99 432
            ->setNotBefore($now)
100 432
            ->setId($identity->getId());
101 432
        if ($duration > 0) {
102 324
            $builder->setExpiration($now + $duration);
103
        }
104 432
        $issuer = $this->getPrincipal($this->issuer);
105 432
        if ($issuer !== null) {
106 432
            $builder->setIssuer($issuer);
107
        }
108 432
        $audience = $this->getPrincipal($this->audience);
109 432
        if ($audience !== null) {
110 432
            $builder->setAudience($audience);
111
        }
112 432
        if ($this->useAuthKey && !$this->appendAuthKey) {
113 144
            $builder->set('authKey', $identity->getAuthKey());
114
        }
115 432
        $this->sendToken($builder, $identity);
116 432
    }
117
118
    /**
119
     * @inheritDoc
120
     */
121 768
    protected function getIdentityAndDurationFromCookie()
122
    {
123
        try {
124
            /** @var IdentityInterface $identity */
125
            /** @var Token $token */
126 768
            list($identity, $token) = $this->getIdentityAndTokenFromCookie();
127 748
        } catch (\Exception $e) {
128 748
            if ($e instanceof InvalidValueException) {
129
                throw $e;
130
            }
131
132 748
            $ip = \Yii::$app->getRequest()->getUserIP();
133 748
            $error = lcfirst($e->getMessage());
134 748
            \Yii::warning("Invalid JWT cookie from $ip: $error", __METHOD__);
135 748
            $this->removeIdentityCookie();
136 748
            return null;
137
        }
138 20
        if ($identity === null) {
139 4
            $this->removeIdentityCookie();
140 4
            return null;
141
        }
142
143 16
        return ['identity' => $identity, 'duration' => $token->hasClaim('exp') ? $token->getClaim('exp') - $token->getClaim('nbf') : 0];
144
    }
145
146
    /**
147
     * @return array|null
148
     */
149 1974
    private function getIdentityAndTokenFromCookie()
150
    {
151 1974
        $value = \Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
152 1974
        if ($value === null) {
153 1
            return null;
154
        }
155
156 1973
        $token = (new Parser())->parse($value);
157 1973
        if ($this->useAuthKey && $this->appendAuthKey) {
158 657
            $identity = $this->getIdentityFromToken($token);
159 657
            if ($identity === null) {
160 1
                return null;
161
            }
162
163 656
            $this->assertSignature($token, $identity);
164 200
            $this->assertClaims($token);
165
        } else {
166 1317
            $this->assertSignature($token);
167 658
            $this->assertClaims($token);
168 178
            $identity = $this->getIdentityFromToken($token);
169 177
            if ($identity === null) {
170 9
                return null;
171
            }
172
        }
173 248
        return [$identity, $token];
174
    }
175
176
    /**
177
     * @param \Closure|string|null $value
178
     * @return string|null
179
     */
180 1074
    private function getPrincipal($value)
181
    {
182 1074
        if (is_string($value)) {
183 520
            return $value;
184
        }
185
186 986
        if ($value instanceof \Closure) {
187 720
            return $value();
188
        }
189
190 522
        return \Yii::$app->getRequest()->getHostInfo();
191
    }
192
193
    /**
194
     * @param IdentityInterface|null $identity
195
     * @return string
196
     */
197 1973
    private function getKey(IdentityInterface $identity = null)
198
    {
199 1973
        $key = (string) $this->key;
200 1973
        if ($this->useAuthKey && $this->appendAuthKey) {
201 656
            $key .= $identity->getAuthKey();
0 ignored issues
show
Bug introduced by
It seems like $identity is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
202
        }
203 1973
        if ($key === '') {
204 3
            throw new InvalidValueException('Sign key cannot be empty.');
205
        }
206
207 1970
        return $key;
208
    }
209
210
    /**
211
     * @param Token $token
212
     * @param IdentityInterface|null $identity
213
     */
214 1973
    private function assertSignature(Token $token, IdentityInterface $identity = null)
215
    {
216 1973
        $key = $identity === null ? $this->getKey() : $this->getKey($identity);
217 1970
        if (!$token->verify(new Sha256(), $key)) {
218 1112
            throw new \InvalidArgumentException('Invalid signature');
219
        }
220 858
    }
221
222
    /**
223
     * @param Token $token
224
     */
225 858
    private function assertClaims(Token $token)
226
    {
227 858
        $validationData = new ValidationData(time());
228 858
        $issuer = $this->getPrincipal($this->issuer);
229 858
        if ($issuer !== null) {
230 858
            $validationData->setIssuer($issuer);
231
        }
232 858
        $audience = $this->getPrincipal($this->audience);
233 858
        if ($audience !== null) {
234 858
            $validationData->setAudience($audience);
235
        }
236 858
        if (!$token->validate($validationData)) {
237 600
            throw new \InvalidArgumentException('Invalid claims');
238
        }
239 258
    }
240
241
    /**
242
     * @param Token $token
243
     * @return IdentityInterface|null
244
     */
245 834
    private function getIdentityFromToken(Token $token)
246
    {
247
        /* @var $class IdentityInterface */
248 834
        $class = $this->identityClass;
249 834
        $id = $token->getClaim('jti');
250 834
        $identity = $class::findIdentity($id);
251 834
        if ($identity === null) {
252 1
            return null;
253
        }
254
255 833
        if (!$identity instanceof IdentityInterface) {
256 1
            throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
257
        }
258
259 832
        if ($this->useAuthKey && !$this->appendAuthKey) {
260 88
            $authKey = $token->getClaim('authKey');
261 88
            if (!$identity->validateAuthKey($authKey)) {
262 8
                \Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__);
263 8
                return null;
264
            }
265
        }
266
267 824
        return $identity;
268
    }
269
270
    /**
271
     * @param Token $token
272
     * @return Builder
273
     */
274 216
    private function createBuilderFromToken(Token $token)
275
    {
276 216
        $builder = new Builder();
277 216
        foreach (array_keys($token->getClaims()) as $name) {
278 216
            $builder->set($name, $token->getClaim($name));
279
        }
280 216
        return $builder;
281
    }
282
283
    /**
284
     * @param Builder $builder
285
     * @param IdentityInterface $identity
286
     */
287 432
    private function sendToken(Builder $builder, IdentityInterface $identity)
288
    {
289 432
        $cookie = new Cookie($this->identityCookie);
290 432
        $cookie->expire = $builder->getToken()->getClaim('exp', '0');
291 432
        $cookie->value = (string) $builder
292 432
            ->sign(new Sha256(), $this->getKey($identity))
293 432
            ->getToken();
294 432
        \Yii::$app->getResponse()->getCookies()->add($cookie);
295 432
    }
296
}
297