Completed
Push — master ( ba29ff...5e737c )
by Sergey
02:58
created

User::setToken()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 15
cts 15
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 6
crap 2
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 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\ValidationData;
16
use yii\base\InvalidValueException;
17
use yii\web\Cookie;
18
use yii\web\IdentityInterface;
19
20
/**
21
 * User class with a JWT cookie as a backend.
22
 *
23
 * @see https://jwt.io
24
 * @see https://tools.ietf.org/html/rfc7519
25
 * @see \yii\web\User
26
 */
27
class User extends \yii\web\User
28
{
29
    /**
30
     * @var string JWT sign key. Must be random and secret.
31
     * @see https://tools.ietf.org/html/rfc7519#section-11
32
     */
33
    public $token;
34
35
    /**
36
     * @var \Closure|string JWT audience claim ("aud").
37
     * @see https://tools.ietf.org/html/rfc7519#section-4.1.3
38
     * @since 1.1
39
     */
40
    public $audience;
41
42
    /**
43
     * @inheritDoc
44
     */
45 5
    protected function loginByCookie()
46 1
    {
47 5
        $claims = $this->getTokenClaims();
48 4
        if ($claims === false) {
49 1
            return;
50
        }
51
52
        /** @var IdentityInterface $class */
53 4
        $class = $this->identityClass;
54 4
        $identity = $class::findIdentity($claims['jti']);
55 4
        if (!isset($identity)) {
56 1
            return;
57 3
        } elseif (!$identity instanceof IdentityInterface) {
58 1
            throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
59
        }
60
61 2
        if (isset($claims['exp'])) {
62 1
            $duration = $claims['exp'] - $claims['nbf'];
63 1
        } else {
64 1
            $duration = 0;
65
        }
66 2
        if ($this->beforeLogin($identity, true, $duration)) {
67 3
            $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
68 2
            $id = $claims['jti'];
69 2
            $ip = \Yii::$app->getRequest()->getUserIP();
70 2
            \Yii::info("User '{$id}' logged in from {$ip} via the JWT cookie.", __METHOD__);
71 2
            $this->afterLogin($identity, true, $duration);
72 2
        }
73 2
    }
74
75
    /**
76
     * @inheritDoc
77
     */
78 1
    protected function renewIdentityCookie()
79
    {
80 1
        $claims = $this->getTokenClaims();
81 1
        if ($claims === false) {
82 1
            return;
83
        }
84
85 1
        $now = time();
86 1
        $expiresAt = $now + ($claims['exp'] - $claims['nbf']);
87 1
        $this->setToken($claims['iat'], $now, $expiresAt, $claims['jti'], $claims['iss'], $claims['aud']);
88 1
    }
89
90
    /**
91
     * @inheritDoc
92
     */
93 2
    protected function sendIdentityCookie($identity, $duration)
94
    {
95 2
        $now = time();
96 2
        if ($duration > 0) {
97 2
            $expiresAt = $now + $duration;
98 2
        } else {
99 1
            $expiresAt = 0;
100
        }
101 2
        $this->setToken($now, $now, $expiresAt, $identity->getId());
102 2
    }
103
104
    /**
105
     * Tries to read, verify, validate and return a JWT claims stored in the identity cookie.
106
     * @param int $currentTime
107
     * @param string $audience
108
     * @return array|false
109
     * @since 1.1
110
     */
111 10
    protected function getTokenClaims($currentTime = null, $audience = null)
112
    {
113 10
        $jwt = \Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
114 10
        if (!isset($jwt)) {
115 7
            return false;
116
        }
117
118
        try {
119 10
            $token = (new Parser())->parse($jwt);
120 10
            if (!$token->verify(new Sha256(), $this->token)) {
121 5
                throw new InvalidValueException('Invalid signature');
122
            }
123
124 10
            if (!$token->validate($this->initClaims(new ValidationData($currentTime), null, $audience))) {
0 ignored issues
show
Bug introduced by
It seems like $this->initClaims(new \L...Time), null, $audience) targeting sergeymakinen\yii\jwtuser\User::initClaims() can also be of type object<Lcobucci\JWT\Builder>; however, Lcobucci\JWT\Token::validate() does only seem to accept object<Lcobucci\JWT\ValidationData>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
125 4
                throw new InvalidValueException('Invalid claims');
126
            }
127 6
            $claims = [];
128 6
            foreach (array_keys($token->getClaims()) as $name) {
129 6
                $claims[$name] = $token->getClaim($name);
130 6
            }
131 6
            return $claims;
132 5
        } catch (\Exception $e) {
133 5
            $error = $e->getMessage();
134
        }
135 5
        $ip = \Yii::$app->getRequest()->getUserIP();
136 5
        \Yii::warning("Invalid JWT cookie from {$ip}: {$error}.", __METHOD__);
137 5
        \Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
138 5
        return false;
139
    }
140
141
    /**
142
     * Writes a JWT token into the identity cookie.
143
     * @param int $issuedAt
144
     * @param int $notBefore
145
     * @param int $expiresAt
146
     * @param mixed $id
147
     * @param string $issuer
148
     * @param string $audience
149
     * @since 1.1
150
     */
151 11
    protected function setToken($issuedAt, $notBefore, $expiresAt, $id, $issuer = null, $audience = null)
152
    {
153 11
        $builder = $this->initClaims(new Builder(), $issuer, $audience)
154 11
            ->setIssuedAt($issuedAt)
155 11
            ->setNotBefore($notBefore)
156 11
            ->setId($id);
157 11
        $cookie = new Cookie($this->identityCookie);
158 11
        if ($expiresAt > 0) {
159 10
            $builder->setExpiration($expiresAt);
160 10
            $cookie->expire = $expiresAt;
161 10
        }
162 11
        $cookie->value = (string) $builder
163 11
            ->sign(new Sha256(), $this->token)
164 11
            ->getToken();
165 11
        \Yii::$app->getResponse()->getCookies()->add($cookie);
166 11
    }
167
168
    /**
169
     * Returns a JWT audience claim ("aud").
170
     * @return string
171
     * @since 1.1
172
     */
173 7
    protected function getAudience()
174
    {
175 7
        if (is_string($this->audience)) {
176 1
            return $this->audience;
177 7
        } elseif ($this->audience instanceof \Closure) {
178 1
            return call_user_func($this->audience);
179
        } else {
180 7
            return \Yii::$app->getRequest()->getHostInfo();
181
        }
182
    }
183
184
    /**
185
     * Returns Builder/ValidationData with "iss" and "aud" claims set.
186
     * @param Builder|ValidationData $object
187
     * @param string $issuer
188
     * @param string $audience
189
     * @return Builder|ValidationData
190
     */
191 11
    private function initClaims($object, $issuer = null, $audience = null)
192
    {
193 11
        if ($object instanceof Builder) {
194 11
            $object->setIssuer(isset($issuer) ? $issuer : \Yii::$app->getRequest()->getHostInfo());
195 11
        }
196 11
        $object->setAudience(isset($audience) ? $audience : $this->getAudience());
197 11
        return $object;
198
    }
199
}
200