Completed
Push — master ( 59eb2e...de7b71 )
by Charles
04:12
created

User::validatePassword()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 4
nop 1
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);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return static::findOne($id); (yii\db\ActiveRecordInterface|array|null) is incompatible with the return type declared by the interface yii\web\IdentityInterface::findIdentity of type yii\web\IdentityInterface.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return static::find()->w...oken->user_id))->one(); (yii\db\ActiveRecord|array|null) is incompatible with the return type declared by the interface yii\web\IdentityInterfac...ndIdentityByAccessToken of type yii\web\IdentityInterface.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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