Passed
Push — master ( 90f2a5...c35d24 )
by Sergey
02:14
created

User::setToken()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 6
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 The MIT License
8
 */
9
10
namespace sergeymakinen\web;
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
     * JWT sign key. Must be random and secret.
31
     *
32
     * @var string
33
     * @see https://tools.ietf.org/html/rfc7519#section-11
34
     */
35
    public $token;
36
37
    /**
38
     * JWT audience claim ("aud").
39
     *
40
     * @var \Closure|string
41
     * @see https://tools.ietf.org/html/rfc7519#section-4.1.3
42
     * @since 1.1
43
     */
44
    public $audience;
45
46
    /**
47
     * @inheritDoc
48
     */
49
    protected function loginByCookie()
50
    {
51
        $claims = $this->getTokenClaims();
52
        if ($claims === false) {
53
            return;
54
        }
55
56
        /** @var IdentityInterface $class */
57
        $class = $this->identityClass;
58
        $identity = $class::findIdentity($claims['jti']);
59
        if (!isset($identity)) {
60
            return;
61
        } elseif (!$identity instanceof IdentityInterface) {
62
            throw new InvalidValueException("$class::findIdentity() must return an object implementing IdentityInterface.");
63
        }
64
65
        if (isset($claims['exp'])) {
66
            $duration = $claims['exp'] - $claims['nbf'];
67
        } else {
68
            $duration = 0;
69
        }
70
        if ($this->beforeLogin($identity, true, $duration)) {
71
            $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0);
72
            $id = $claims['jti'];
73
            $ip = \Yii::$app->getRequest()->getUserIP();
74
            \Yii::info("User '{$id}' logged in from {$ip} via the JWT cookie.", __METHOD__);
75
            $this->afterLogin($identity, true, $duration);
76
        }
77
    }
78
79
    /**
80
     * @inheritDoc
81
     */
82
    protected function renewIdentityCookie()
83
    {
84
        $claims = $this->getTokenClaims();
85
        if ($claims === false) {
86
            return;
87
        }
88
89
        $now = time();
90
        $expiresAt = $now + ($claims['exp'] - $claims['nbf']);
91
        $this->setToken($claims['iat'], $now, $expiresAt, $claims['jti'], $claims['iss'], $claims['aud']);
92
    }
93
94
    /**
95
     * @inheritDoc
96
     */
97
    protected function sendIdentityCookie($identity, $duration)
98
    {
99
        $now = time();
100
        if ($duration > 0) {
101
            $expiresAt = $now + $duration;
102
        } else {
103
            $expiresAt = 0;
104
        }
105
        $this->setToken($now, $now, $expiresAt, $identity->getId());
106
    }
107
108
    /**
109
     * Tries to read, verify, validate and return a JWT claims stored in the identity cookie.
110
     *
111
     * @return array|false
112
     * @since 1.1
113
     */
114
    protected function getTokenClaims()
115
    {
116
        $jwt = \Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
117
        if (!isset($jwt)) {
118
            return false;
119
        }
120
121
        try {
122
            $token = (new Parser())->parse($jwt);
123
            if (!$token->verify(new Sha256(), $this->token)) {
124
                throw new InvalidValueException('Invalid signature');
125
            }
126
127
            if (!$token->validate($this->initClaims(new ValidationData()))) {
1 ignored issue
show
Bug introduced by
It seems like $this->initClaims(new \L...i\JWT\ValidationData()) targeting sergeymakinen\web\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...
128
                throw new InvalidValueException('Invalid claims');
129
            }
130
            $claims = [];
131
            foreach (array_keys($token->getClaims()) as $name) {
132
                $claims[$name] = $token->getClaim($name);
133
            }
134
            return $claims;
135
        } catch (\Exception $e) {
136
            $error = $e->getMessage();
137
        }
138
        $ip = \Yii::$app->getRequest()->getUserIP();
139
        \Yii::warning("Invalid JWT cookie from {$ip}: {$error}.", __METHOD__);
140
        \Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie));
141
        return false;
142
    }
143
144
    /**
145
     * Writes a JWT token into the identity cookie.
146
     *
147
     * @param int $issuedAt
148
     * @param int $notBefore
149
     * @param int $expiresAt
150
     * @param mixed $id
151
     * @param string $issuer
152
     * @param string $audience
153
     * @since 1.1
154
     */
155
    protected function setToken($issuedAt, $notBefore, $expiresAt, $id, $issuer = null, $audience = null)
156
    {
157
        $builder = $this->initClaims(new Builder(), $issuer, $audience)
1 ignored issue
show
Bug introduced by
The method setIssuedAt does only exist in Lcobucci\JWT\Builder, but not in Lcobucci\JWT\ValidationData.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
158
            ->setIssuedAt($issuedAt)
159
            ->setNotBefore($notBefore)
160
            ->setId($id);
161
        $cookie = new Cookie($this->identityCookie);
162
        if ($expiresAt > 0) {
163
            $builder->setExpiration($expiresAt);
164
            $cookie->expire = $expiresAt;
165
        }
166
        $cookie->value = (string) $builder
167
            ->sign(new Sha256(), $this->token)
168
            ->getToken();
169
        \Yii::$app->getResponse()->getCookies()->add($cookie);
170
    }
171
172
    /**
173
     * Returns a JWT audience claim ("aud").
174
     *
175
     * @return string
176
     * @since 1.1
177
     */
178
    protected function getAudience()
179
    {
180
        if (is_string($this->audience)) {
181
            return $this->audience;
182
        } elseif ($this->audience instanceof \Closure) {
183
            return call_user_func($this->audience);
184
        } else {
185
            return \Yii::$app->getRequest()->getHostInfo();
186
        }
187
    }
188
189
    /**
190
     * Returns Builder/ValidationData with "iss" and "aud" claims set.
191
     *
192
     * @param Builder|ValidationData $object
193
     * @param string $issuer
194
     * @param string $audience
195
     *
196
     * @return Builder|ValidationData
197
     */
198
    private function initClaims($object, $issuer = null, $audience = null)
199
    {
200
        if ($object instanceof Builder) {
201
            $object->setIssuer(isset($issuer) ? $issuer : \Yii::$app->getRequest()->getHostInfo());
202
        }
203
        $object->setAudience(isset($audience) ? $audience : $this->getAudience());
204
        return $object;
205
    }
206
}
207