1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace yrc\models; |
4
|
|
|
|
5
|
|
|
use Base32\Base32; |
6
|
|
|
use OTPHP\TOTP; |
7
|
|
|
|
8
|
|
|
use yii\behaviors\TimestampBehavior; |
9
|
|
|
use yii\db\ActiveRecord; |
10
|
|
|
use yii\filters\RateLimitInterface; |
11
|
|
|
use yii\web\IdentityInterface; |
12
|
|
|
use Yii; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* This is the model class for table "user". |
16
|
|
|
* |
17
|
|
|
* @property integer $id |
18
|
|
|
* @property string $email |
19
|
|
|
* @property strin $username |
20
|
|
|
* @property string $password |
21
|
|
|
* @property string $activation_token |
22
|
|
|
* @property string $reset_token |
23
|
|
|
* @property integer $verified |
24
|
|
|
* @property string $otp_secret |
25
|
|
|
* @property string $otp_enabled |
26
|
|
|
* @property integer $created_at |
27
|
|
|
* @property integer $updated_at |
28
|
|
|
*/ |
29
|
|
|
abstract class User extends ActiveRecord implements IdentityInterface, RateLimitInterface |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* password_hash Algorithm |
33
|
|
|
* @var integer |
34
|
|
|
*/ |
35
|
|
|
private $passwordHashAlgorithm = PASSWORD_BCRYPT; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* The rate limit |
39
|
|
|
* @var integer |
40
|
|
|
*/ |
41
|
|
|
private $rateLimit = 150; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* The rate limit window |
45
|
|
|
* @var integer |
46
|
|
|
*/ |
47
|
|
|
private $rateLimitWindow = 900; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* password_hash options |
51
|
|
|
* @var array |
52
|
|
|
*/ |
53
|
|
|
private $passwordHashOptions = [ |
54
|
|
|
'cost' => 13, |
55
|
|
|
'memory_cost' => 1<<12, |
56
|
|
|
'time_cost' => 3, |
57
|
|
|
'threads' => 1 |
58
|
|
|
]; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* The token used to authenticate the user |
62
|
|
|
* @var app\models\Token |
63
|
|
|
*/ |
64
|
|
|
protected $token; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Sets the token used to authenticate the user |
68
|
|
|
* @return app\models\Token |
69
|
|
|
*/ |
70
|
|
|
public function getToken() |
71
|
|
|
{ |
72
|
|
|
return $this->token; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Sets the token that was used to authenticate the user |
77
|
|
|
*/ |
78
|
|
|
public function setToken($token) |
79
|
|
|
{ |
80
|
|
|
if ($this->token !== null) { |
81
|
|
|
throw new \yii\base\Exception(Yii::t('yrc', 'The user has already been authenticated.')); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
$this->token = $token; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Overrides init |
89
|
|
|
*/ |
90
|
|
|
public function init() |
91
|
|
|
{ |
92
|
|
|
// self init |
93
|
|
|
parent::init(); |
94
|
|
|
|
95
|
|
|
// Prefer Argon2 if it is available, but fall back to BCRYPT if it isn't |
96
|
|
|
if (defined('PASSWORD_ARGON2I')) { |
97
|
|
|
$this->passwordHashAlgorithm = PASSWORD_ARGON2I; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// Lower the bcrypt cost when running tests |
101
|
|
|
if (YII_DEBUG && $this->passwordHashAlgorithm === PASSWORD_BCRYPT) { |
102
|
|
|
$this->passwordHashOptions['cost'] = 10; |
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @inheritdoc |
108
|
|
|
*/ |
109
|
|
|
public function behaviors() |
110
|
|
|
{ |
111
|
|
|
return [ |
112
|
|
|
TimestampBehavior::className(), |
113
|
|
|
]; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @inheritdoc |
118
|
|
|
*/ |
119
|
|
|
public function getRateLimit($request, $action) |
120
|
|
|
{ |
121
|
|
|
return [ |
122
|
|
|
$this->rateLimit, |
123
|
|
|
$this->rateLimitWindow |
124
|
|
|
]; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @inheritdoc |
129
|
|
|
*/ |
130
|
|
|
public function loadAllowance($request, $action) |
131
|
|
|
{ |
132
|
|
|
$hash = Yii::$app->user->id . $request->getUrl() . $action->id; |
133
|
|
|
$allowance = Yii::$app->cache->get($hash); |
134
|
|
|
|
135
|
|
|
if ($allowance === false) { |
136
|
|
|
return [ |
137
|
|
|
$this->rateLimit, |
138
|
|
|
time() |
139
|
|
|
]; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
return $allowance; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @inheritdoc |
147
|
|
|
*/ |
148
|
|
|
public function saveAllowance($request, $action, $allowance, $timestamp) |
149
|
|
|
{ |
150
|
|
|
$hash = Yii::$app->user->id . $request->getUrl() . $action->id; |
151
|
|
|
$allowance = [ |
152
|
|
|
$allowance, |
153
|
|
|
$timestamp |
154
|
|
|
]; |
155
|
|
|
|
156
|
|
|
Yii::$app->cache->set($hash, $allowance, $this->rateLimitWindow); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @inheritdoc |
161
|
|
|
*/ |
162
|
|
|
public static function tableName() |
163
|
|
|
{ |
164
|
|
|
return 'user'; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @inheritdoc |
169
|
|
|
*/ |
170
|
|
|
public function rules() |
171
|
|
|
{ |
172
|
|
|
return [ |
173
|
|
|
[['password', 'email', 'username'], 'required'], |
174
|
|
|
[['email'], 'email'], |
175
|
|
|
[['password'], 'string', 'length' => [8, 100]], |
176
|
|
|
[['username'], 'string', 'length' => [1, 100]], |
177
|
|
|
[['created_at', 'updated_at', 'otp_enabled', 'verified'], 'integer'], |
178
|
|
|
[['password', 'email'], 'string', 'max' => 255], |
179
|
|
|
[['email'], 'unique'], |
180
|
|
|
[['username'], 'unique'], |
181
|
|
|
]; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @inheritdoc |
186
|
|
|
*/ |
187
|
|
|
public function attributeLabels() |
188
|
|
|
{ |
189
|
|
|
return [ |
190
|
|
|
'id' => 'ID', |
191
|
|
|
'email' => 'Email Address', |
192
|
|
|
'username' => 'Username', |
193
|
|
|
'password' => 'Password', |
194
|
|
|
'activation_token' => 'Activation Token', |
195
|
|
|
'otp_secret' => 'One Time Password Secret Value', |
196
|
|
|
'otp_enabled' => 'Is Two Factor Authentication Enabled?', |
197
|
|
|
'verified' => 'Is the account email verified?', |
198
|
|
|
'created_at' => 'Created At', |
199
|
|
|
'updated_at' => 'Last Updated At' |
200
|
|
|
]; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
public function beforeValidate() |
204
|
|
|
{ |
205
|
|
|
if (parent::beforeValidate()) { |
206
|
|
|
$this->username = \strtolower($this->username); |
207
|
|
|
return true; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
return false; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Before save occurs |
215
|
|
|
* @return bool |
216
|
|
|
*/ |
217
|
|
|
public function beforeSave($insert) |
218
|
|
|
{ |
219
|
|
|
if (parent::beforeSave($insert)) { |
220
|
|
|
if ($this->isNewRecord || $this->password !== $this->oldAttributes['password']) { |
221
|
|
|
$this->password = password_hash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
return true; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
return false; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Validates the user's password |
232
|
|
|
* @param string $password |
233
|
|
|
* return bool |
234
|
|
|
*/ |
235
|
|
|
public function validatePassword($password) |
236
|
|
|
{ |
237
|
|
|
if (password_verify($password, $this->password)) { |
238
|
|
|
if (password_needs_rehash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions)) { |
239
|
|
|
$this->password = password_hash($password, $this->passwordHashAlgorithm, $this->passwordHashOptions); |
240
|
|
|
|
241
|
|
|
// Allow authentication to continue if we weren't able to update the password, but log the message |
242
|
|
|
if (!$this->save()) { |
243
|
|
|
Yii::warning('Unable to save newly hashed password for user: ' . $this->id); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
return true; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return false; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Returns true of OTP is enabled |
255
|
|
|
* @return boolean |
256
|
|
|
*/ |
257
|
|
|
public function isOTPEnabled() |
258
|
|
|
{ |
259
|
|
|
return (bool)$this->otp_enabled; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Provisions TOTP for the account |
264
|
|
|
* @return boolean|string |
265
|
|
|
*/ |
266
|
|
|
public function provisionOTP() |
267
|
|
|
{ |
268
|
|
|
if ($this->isOTPEnabled() === true) { |
269
|
|
|
return false; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$secret = \random_bytes(256); |
273
|
|
|
$encodedSecret = Base32::encode($secret); |
274
|
|
|
$totp = TOTP::create( |
275
|
|
|
$encodedSecret, |
276
|
|
|
30, // 30 second window |
277
|
|
|
'sha256', // SHA256 for the hashing algorithm |
278
|
|
|
6 // 6 digits |
279
|
|
|
); |
280
|
|
|
$totp->setLabel($this->username); |
281
|
|
|
|
282
|
|
|
$this->otp_secret = $encodedSecret; |
283
|
|
|
|
284
|
|
|
if ($this->save()) { |
285
|
|
|
return $totp->getProvisioningUri(); |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
return false; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Enables OTP |
293
|
|
|
* @return boolean |
294
|
|
|
*/ |
295
|
|
|
public function enableOTP() |
296
|
|
|
{ |
297
|
|
|
if ($this->isOTPEnabled() === true) { |
298
|
|
|
return true; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
if ($this->otp_secret == "") { |
302
|
|
|
return false; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
$this->otp_enabled = 1; |
306
|
|
|
|
307
|
|
|
return $this->save(); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Disables OTP |
312
|
|
|
* @return boolean |
313
|
|
|
*/ |
314
|
|
|
public function disableOTP() |
315
|
|
|
{ |
316
|
|
|
$this->otp_secret = ""; |
317
|
|
|
$this->otp_enabled = 0; |
318
|
|
|
|
319
|
|
|
return $this->save(); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Verifies the OTP code |
324
|
|
|
* @param integer $code |
325
|
|
|
* @return boolean |
326
|
|
|
*/ |
327
|
|
|
public function verifyOTP($code) |
328
|
|
|
{ |
329
|
|
|
$totp = TOTP::create( |
330
|
|
|
$this->otp_secret, |
331
|
|
|
30, // 30 second window |
332
|
|
|
'sha256', // SHA256 for the hashing algorithm |
333
|
|
|
6 // 6 digits |
334
|
|
|
); |
335
|
|
|
|
336
|
|
|
$totp->setLabel($this->username); |
337
|
|
|
|
338
|
|
|
return $totp->verify($code); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Activates the user |
343
|
|
|
* @return boolean |
344
|
|
|
*/ |
345
|
|
|
public function activate() |
346
|
|
|
{ |
347
|
|
|
$this->verified = 1; |
348
|
|
|
return $this->save(); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Whether or not a user is activated or not |
353
|
|
|
* @return boolean |
354
|
|
|
*/ |
355
|
|
|
public function isActivated() |
356
|
|
|
{ |
357
|
|
|
return (bool)$this->verified; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @inheritdoc |
362
|
|
|
*/ |
363
|
|
|
public static function findIdentity($id) |
364
|
|
|
{ |
365
|
|
|
return static::findOne($id); |
|
|
|
|
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* @inheritdoc |
370
|
|
|
*/ |
371
|
|
|
public static function findIdentityByAccessToken($token, $type = null) |
372
|
|
|
{ |
373
|
|
|
// Checking of the Token is performed in app\components\filters\auth\HMACSignatureAuth |
374
|
|
|
if ($token === null) { |
375
|
|
|
return null; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
return static::find()->where(['id' => $token->user_id])->one(); |
|
|
|
|
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* @inheritdoc |
383
|
|
|
*/ |
384
|
|
|
public function getAuthKey() |
385
|
|
|
{ |
386
|
|
|
|
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
/** |
390
|
|
|
* @inheritdoc |
391
|
|
|
*/ |
392
|
|
|
public function validateAuthKey($authKey) |
393
|
|
|
{ |
394
|
|
|
return true; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
/** |
398
|
|
|
* @inheritdoc |
399
|
|
|
*/ |
400
|
|
|
public function getId() |
401
|
|
|
{ |
402
|
|
|
return $this->id; |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.