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()))) { |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.