Passed
Push — master ( 436bae...07feba )
by Charles
04:31
created

User::rules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 11
rs 9.9666
c 0
b 0
f 0
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 string $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
    const TOKEN_CLASS = '\yrc\models\redis\Token';
32
    
33
    /**
34
     * password_hash Algorithm
35
     * @var integer
36
     */
37
    private $passwordHashAlgorithm = PASSWORD_BCRYPT;
38
    
39
    /**
40
     * The rate limit
41
     * @var integer
42
     */
43
    private $rateLimit = 150;
44
45
    /**
46
     * The rate limit window
47
     * @var integer
48
     */
49
    private $rateLimitWindow = 900;
50
    
51
    /**
52
     * password_hash options
53
     * @var array
54
     */
55
    private $passwordHashOptions = [
56
        'cost' => 13,
57
        'memory_cost' => 1<<12,
58
        'time_cost' => 3,
59
        'threads' => 1
60
    ];
61
    
62
    /**
63
     * The token used to authenticate the user
64
     * @var app\models\Token
0 ignored issues
show
Bug introduced by
The type yrc\models\app\models\Token was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
65
     */
66
    protected $token;
67
68
    /**
69
     * Sets the token used to authenticate the user
70
     * @return app\models\Token
71
     */
72
    public function getToken()
73
    {
74
        return $this->token;
75
    }
76
77
    /**
78
     * Sets the token that was used to authenticate the user
79
     */
80
    public function setToken($token)
81
    {
82
        if ($this->token !== null) {
83
            throw new \yii\base\Exception(Yii::t('yrc', 'The user has already been authenticated.'));
84
        }
85
86
        $this->token = $token;
87
    }
88
89
    /**
90
     * Overrides init
91
     */
92
    public function init()
93
    {
94
        // self init
95
        parent::init();
96
97
        // Prefer Argon2 if it is available, but fall back to BCRYPT if it isn't
98
        if (defined('PASSWORD_ARGON2ID')) {
99
            $this->passwordHashAlgorithm = PASSWORD_ARGON2ID;
1 ignored issue
show
Bug introduced by
The constant yrc\models\PASSWORD_ARGON2ID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
100
        } elseif (defined('PASSWORD_ARGON2I')) {
101
            $this->passwordHashAlgorithm = PASSWORD_ARGON2I;
102
        }
103
104
        // Lower the bcrypt cost when running tests
105
        if (YII_DEBUG && $this->passwordHashAlgorithm === PASSWORD_BCRYPT) {
106
            $this->passwordHashOptions['cost'] = 10;
107
        }
108
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113
    public function behaviors()
114
    {
115
        return [
116
            TimestampBehavior::class,
117
        ];
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123
    public function getRateLimit($request, $action)
124
    {
125
        return [
126
            $this->rateLimit,
127
            $this->rateLimitWindow
128
        ];
129
    }
130
131
    /**
132
     * @inheritdoc
133
     */
134
    public function loadAllowance($request, $action)
135
    {
136
        $hash = Yii::$app->user->id . $request->getUrl() . $action->id;
137
        $allowance = Yii::$app->cache->get($hash);
138
139
        if ($allowance === false) {
140
            return [
141
                $this->rateLimit,
142
                time()
143
            ];
144
        }
145
146
        return $allowance;
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152
    public function saveAllowance($request, $action, $allowance, $timestamp)
153
    {
154
        $hash = Yii::$app->user->id . $request->getUrl() . $action->id;
155
        $allowance = [
156
            $allowance,
157
            $timestamp
158
        ];
159
160
        Yii::$app->cache->set($hash, $allowance, $this->rateLimitWindow);
161
    }
162
    
163
    /**
164
     * @inheritdoc
165
     */
166
    public static function tableName()
167
    {
168
        return 'user';
169
    }
170
171
    /**
172
     * @inheritdoc
173
     */
174
    public function rules()
175
    {
176
        return [
177
            [['password', 'email', 'username'], 'required'],
178
            [['email'], 'email'],
179
            [['password'], 'string', 'length' => [8, 100]],
180
            [['username'], 'string', 'length' => [1, 100]],
181
            [['created_at', 'updated_at', 'otp_enabled', 'verified'], 'integer'],
182
            [['password', 'email'], 'string', 'max' => 255],
183
            [['email'], 'unique'],
184
            [['username'], 'unique'],
185
        ];
186
    }
187
188
    /**
189
     * @inheritdoc
190
     */
191
    public function attributeLabels()
192
    {
193
        return [
194
            'id'                => 'ID',
195
            'email'             => 'Email Address',
196
            'username'          => 'Username',
197
            'password'          => 'Password',
198
            'activation_token'  => 'Activation Token',
199
            'otp_secret'        => 'One Time Password Secret Value',
200
            'otp_enabled'       => 'Is Two Factor Authentication Enabled?',
201
            'verified'          => 'Is the account email verified?',
202
            'created_at'        => 'Created At',
203
            'updated_at'        => 'Last Updated At'
204
        ];
205
    }
206
207
    public function beforeValidate()
208
    {
209
        if (parent::beforeValidate()) {
210
            $this->username = \strtolower($this->username);
211
            return true;
212
        }
213
214
        return false;
215
    }
216
217
    /**
218
     * Before save occurs
219
     * @return bool
220
     */
221
    public function beforeSave($insert)
222
    {
223
        if (parent::beforeSave($insert)) {
224
            if ($this->isNewRecord || $this->password !== $this->oldAttributes['password']) {
225
                $this->password = password_hash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
226
            }
227
            
228
            return true;
229
        }
230
        
231
        return false;
232
    }
233
    
234
    /**
235
     * Validates the user's password
236
     * @param string $password
237
     * return bool
238
     */
239
    public function validatePassword($password)
240
    {
241
        if (password_verify($password, $this->password)) {
242
            if (password_needs_rehash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions)) {
243
                $this->password = password_hash($password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
244
                
245
                // Allow authentication to continue if we weren't able to update the password, but log the message
246
                if (!$this->save()) {
247
                    Yii::warning('Unable to save newly hashed password for user: ' . $this->id);
248
                }
249
            }
250
251
            return true;
252
        }
253
        
254
        return false;
255
    }
256
257
    /**
258
     * Returns true of OTP is enabled
259
     * @return boolean
260
     */
261
    public function isOTPEnabled()
262
    {
263
        return (bool)$this->otp_enabled;
264
    }
265
    
266
    /**
267
     * Provisions TOTP for the account
268
     * @return boolean|string
269
     */
270
    public function provisionOTP()
271
    {
272
        if ($this->isOTPEnabled() === true) {
273
            return false;
274
        }
275
276
        $secret = \random_bytes(256);
277
        $encodedSecret = Base32::encode($secret);
278
        $totp = TOTP::create(
279
            $encodedSecret,
280
            30,             // 30 second window
281
            'sha256',       // SHA256 for the hashing algorithm
282
            6               // 6 digits
283
        );
284
        $totp->setLabel($this->username);
285
286
        $this->otp_secret = $encodedSecret;
287
288
        if ($this->save()) {
289
            return $totp->getProvisioningUri();
290
        }
291
292
        return false;
293
    }
294
295
    /**
296
     * Enables OTP
297
     * @return boolean
298
     */
299
    public function enableOTP()
300
    {
301
        if ($this->isOTPEnabled() === true) {
302
            return true;
303
        }
304
305
        if ($this->otp_secret == "") {
306
            return false;
307
        }
308
        
309
        $this->otp_enabled = 1;
310
311
        return $this->save();
312
    }
313
314
    /**
315
     * Disables OTP
316
     * @return boolean
317
     */
318
    public function disableOTP()
319
    {
320
        $this->otp_secret = "";
321
        $this->otp_enabled = 0;
322
323
        return $this->save();
324
    }
325
326
    /**
327
     * Verifies the OTP code
328
     * @param integer $code
329
     * @return boolean
330
     */
331
    public function verifyOTP($code)
332
    {
333
        $totp = TOTP::create(
334
            $this->otp_secret,
335
            30,             // 30 second window
336
            'sha256',       // SHA256 for the hashing algorithm
337
            6               // 6 digits
338
        );
339
340
        $totp->setLabel($this->username);
341
342
        return $totp->verify($code);
343
    }
344
345
    /**
346
     * Activates the user
347
     * @return boolean
348
     */
349
    public function activate()
350
    {
351
        $this->verified = 1;
352
        return $this->save();
353
    }
354
355
    /**
356
     * Whether or not a user is activated or not
357
     * @return boolean
358
     */
359
    public function isActivated()
360
    {
361
        return (bool)$this->verified;
362
    }
363
364
    /**
365
     * @inheritdoc
366
     */
367
    public static function findIdentity($id)
368
    {
369
        return static::findOne($id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::findOne($id) also could return the type yii\db\BaseActiveRecord which is incompatible with the return type mandated by yii\web\IdentityInterface::findIdentity() of yii\web\IdentityInterface.
Loading history...
370
    }
371
    
372
    /**
373
     * @inheritdoc
374
     */
375
    public static function findIdentityByAccessToken($token, $type = null)
376
    {
377
        // Checking of the Token is performed in app\components\filters\auth\HMACSignatureAuth
378
        if ($token === null) {
379
            return null;
380
        }
381
        
382
        return static::find()->where(['id' => $token->user_id])->one();
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::find()->w...token->user_id))->one() also could return the type yii\db\ActiveRecord|array which is incompatible with the return type mandated by yii\web\IdentityInterfac...IdentityByAccessToken() of yii\web\IdentityInterface.
Loading history...
383
    }
384
385
    /**
386
     * @inheritdoc
387
     */
388
    public function getAuthKey()
389
    {
390
    }
391
392
    /**
393
     * @inheritdoc
394
     */
395
    public function validateAuthKey($authKey)
396
    {
397
        return true;
398
    }
399
400
    /**
401
     * @inheritdoc
402
     */
403
    public function getId()
404
    {
405
        return $this->id;
406
    }
407
}
408