Behavior::events()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 3
cts 4
cp 0.75
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2.0625
1
<?php
2
/**
3
 * @link https://github.com/vuongxuongminh/yii2-mfa
4
 * @copyright Copyright (c) 2019 Vuong Xuong Minh
5
 * @license [New BSD License](http://www.opensource.org/licenses/bsd-license.php)
6
 */
7
8
namespace vxm\mfa;
9
10
use Yii;
11
12
use yii\base\Behavior as BaseBehavior;
13
use yii\base\InvalidValueException;
14
use yii\di\Instance;
15
use yii\web\User;
16
use yii\web\UserEvent;
17
use yii\web\ForbiddenHttpException;
18
19
20
/**
21
 * Class MfaBehavior automatically redirect to verify mfa url when identity enabled it and verify digits given.
22
 *
23
 * To use MfaBehavior, configure the [[User::$identityClass]] property which should specify class implemented [[\vxm\mfa\IdentityInterface]].
24
 *
25
 * For example,
26
 *
27
 * ```php
28
 *
29
 * use yii\db\ActiveRecord;
30
 *
31
 * use vxm\mfa\IdentityInterface;
32
 *
33
 * class Identity extends ActiveRecord implements IdentityInterface {
34
 *
35
 *          public function getMfaSecretKey()
36
 *          {
37
 *              return $this->mfa_secret;
38
 *          }
39
 *
40
 * }
41
 * ```
42
 *
43
 * And attach this behavior to the [[User]] component of an application config.
44
 *
45
 * For example,
46
 *
47
 * ```php
48
 *
49
 * 'user' => [
50
 *      'as mfa' => [
51
 *           'class' => 'vxm\mfa\MfaBehavior',
52
 *           'verifyUrl' => 'site/mfa-verify',
53
 *      ]
54
 *
55
 * ]
56
 * ```
57
 *
58
 * Note: it only work when an owner [[User::$enableSession]] is true.
59
 *
60
 * @property Otp $otp use to validate, generating an otp digits.
61
 *
62
 * @author Vuong Minh <[email protected]>
63
 * @since 1.0.0
64
 */
65
class Behavior extends BaseBehavior
66
{
67
68
    /**
69
     * @var User
70
     */
71
    public $owner;
72
73
    /**
74
     * @var callable|bool weather enabling this behavior. This property use in special case you need to disable it in runtime environment.
75
     * When it is callable this object instance will be parse to first parameter.
76
     *
77
     * Example:
78
     * ```php
79
     * function(\vxm\mfa\Behavior $behavior) {
80
     *
81
     *
82
     * }
83
     * ```
84
     */
85
    public $enable = true;
86
87
    /**
88
     * @var string|array the URL for login when [[verifyRequired()]] is called.
89
     * If an array is given, [[\yii\web\UrlManager::createUrl()]] will be called to create the corresponding URL.
90
     * The first element of the array should be the route to the verify action, and the rest of
91
     * the name-value pairs are GET parameters used to construct the verify URL. For example,
92
     *
93
     * ```php
94
     * 'site/mfa-verify'
95
     * ```
96
     *
97
     * If this property is `null`, a 403 HTTP exception will be raised when [[verifyRequired()]] is called.
98
     */
99
    public $verifyUrl;
100
101
    /**
102
     * @var string the session variable name used to store values of an identity logged in.
103
     */
104
    public $mfaParam = '__mfa';
105
106
    /**
107
     * @inheritDoc
108
     */
109 13
    public function init()
110
    {
111 13
        if (is_callable($this->enable)) {
112
            $this->enable = call_user_func($this->enable, $this);
113
        }
114
115 13
        parent::init();
116 13
    }
117
118
    /**
119
     * @inheritDoc
120
     */
121 13
    public function events()
122
    {
123 13
        if ($this->enable) {
124
            return [
125 13
                User::EVENT_BEFORE_LOGIN => 'beforeLogin'
126
            ];
127
        } else {
128
            return [];
129
        }
130
    }
131
132
    /**
133
     * Event trigger when before user log in to system. It will be require an user verify otp digits except when user logged in via cookie base.
134
     *
135
     * @param UserEvent $event an event triggered
136
     * @throws ForbiddenHttpException
137
     */
138 11
    public function beforeLogin(UserEvent $event)
139
    {
140 11
        if (!$event->isValid) {
141
            return;
142
        }
143
144 11
        if (!$event->identity instanceof IdentityInterface) {
145
            throw new InvalidValueException("{$this->owner->identityClass}::findIdentity() must return an object implementing \\vxm\\mfa\\IdentityInterface.");
146
        }
147
148 11
        $secretKey = $event->identity->getMfaSecretKey();
149
150 11
        if (!empty($secretKey) && $this->owner->enableSession && !$event->cookieBased) {
151 11
            $event->isValid = false;
152 11
            $this->saveIdentityLoggedIn($event->identity, $event->duration);
153 11
            $this->verifyRequired();
154
        }
155 11
    }
156
157
    /**
158
     * Switches to a logged in identity for the current user.
159
     *
160
     * @see \yii\web\User::switchIdentity()
161
     */
162 5
    public function switchIdentityLoggedIn()
163
    {
164 5
        $data = $this->getIdentityLoggedIn();
165
166 5
        if ($data === null) {
167
            return;
168
        }
169
170 5
        list($identity, $duration) = $data;
171 5
        $this->owner->switchIdentity($identity, $duration);
172 5
    }
173
174
    /**
175
     * Save the user identity logged in object when an identity need to verify.
176
     *
177
     * @param IdentityInterface|null $identity the identity object associated with the currently logged user.
178
     * @param int $duration number of seconds that the user can remain in logged-in status.
179
     */
180 11
    public function saveIdentityLoggedIn(IdentityInterface $identity, int $duration)
181
    {
182 11
        Yii::$app->getSession()->set($this->mfaParam, [$identity->getId(), $duration]);
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

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...
183 11
    }
184
185
    /**
186
     * Get an identity logged in.
187
     *
188
     * @return array|null Returns an array of 'identity' and 'duration' if valid, otherwise null.
189
     * @see saveIdentityLoggedIn()
190
     */
191 12
    public function getIdentityLoggedIn()
192
    {
193 12
        $data = Yii::$app->getSession()->get($this->mfaParam);
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

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...
194
195 12
        if ($data === null) {
196 2
            return null;
197
        }
198
199 11
        if (is_array($data) && count($data) == 2) {
200 11
            list($id, $duration) = $data;
201
            /* @var $class IdentityInterface */
202 11
            $class = $this->owner->identityClass;
203 11
            $identity = $class::findIdentity($id);
204
205 11
            if ($identity !== null) {
206 11
                if (!$identity instanceof IdentityInterface) {
207
                    throw new InvalidValueException("$class::findIdentity() must return an object implementing \\vxm\\mfa\\IdentityInterface.");
208
                } else {
209 11
                    return [$identity, $duration];
210
                }
211
            }
212
        }
213
214
        $this->removeIdentityLoggedIn();
215
216
        return null;
217
    }
218
219
    /**
220
     * Removes the identity logged in.
221
     */
222 2
    public function removeIdentityLoggedIn()
223
    {
224 2
        Yii::$app->getSession()->remove($this->mfaParam);
0 ignored issues
show
Bug introduced by
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

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...
225 2
    }
226
227
    /**
228
     * @var Otp|null an otp instance use to generate and validate otp.
229
     */
230
    private $_otp;
231
232
    /**
233
     * Get an otp instance.
234
     *
235
     * @return Otp|null an otp instance use to generate and validate otp.
236
     * @throws \yii\base\InvalidConfigException
237
     */
238 8
    public function getOtp()
239
    {
240 8
        if ($this->_otp === null) {
241 7
            $this->setOtp(Otp::class);
242
        }
243
244 8
        return $this->_otp;
245
    }
246
247
    /**
248
     * Set an otp instance use to generate and validate otp.
249
     *
250
     * @param array|string|Otp $otp object instance
251
     * @throws \yii\base\InvalidConfigException
252
     */
253 8
    public function setOtp($otp)
254
    {
255 8
        if (is_array($otp) && !isset($otp['class'])) {
256 1
            $otp['class'] = Otp::class;
257
        }
258
259 8
        $this->_otp = Instance::ensure($otp, Otp::class);
260 8
    }
261
262
    /**
263
     * Generate an otp by current user logged in
264
     *
265
     * @return string|null an otp of current user logged in.
266
     * @throws \yii\base\InvalidConfigException
267
     * @throws \yii\base\NotSupportedException
268
     */
269 4 View Code Duplication
    public function generateOtpByIdentityLoggedIn()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
270
    {
271 4
        $data = $this->getIdentityLoggedIn();
272
273 4
        if (is_array($data)) {
274
275
            /** @var IdentityInterface $identity */
276 4
            $identity = $data[0];
277 4
            $secretKey = $identity->getMfaSecretKey();
278
279 4
            if (!empty($secretKey)) {
280 4
                return $this->getOtp()->generate($secretKey);
281
            }
282
        }
283
284
        return null;
285
    }
286
287
    /**
288
     * Validate an otp by current user logged in
289
     *
290
     * @param string $otp need to be validate
291
     * @return bool weather an otp given is valid with identity logged in
292
     * @throws \yii\base\InvalidConfigException
293
     * @throws \yii\base\NotSupportedException
294
     */
295 3 View Code Duplication
    public function validateOtpByIdentityLoggedIn(string $otp)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
296
    {
297 3
        $data = $this->getIdentityLoggedIn();
298
299 3
        if (is_array($data)) {
300
            /** @var IdentityInterface $identity */
301 3
            $identity = $data[0];
302 3
            $secretKey = $identity->getMfaSecretKey();
303
304 3
            if (!empty($secretKey)) {
305 3
                return $this->getOtp()->validate($secretKey, $otp);
306
            }
307
        }
308
309
        return false;
310
    }
311
312
    /**
313
     * Return a qr code uri of current user
314
     *
315
     * @param array $params list of information use to show on an authenticator app.
316
     *
317
     * Example:
318
     * ```php
319
     * ['issuer' => 'VXM', 'label' => '[email protected]', 'image' => 'https://google.com']
320
     * ```
321
     *
322
     * @return string|null qr code uri. If `null`, it means the user is a guest or not enable mfa.
323
     * @throws \Throwable
324
     */
325 4
    public function getQrCodeUri(array $params)
326
    {
327 4
        if ($identity = $this->owner->getIdentity()) {
328 3
            if (!$identity instanceof IdentityInterface) {
329
                throw new InvalidValueException("{$this->owner->identityClass}::findIdentity() must return an object implementing \\vxm\\mfa\\IdentityInterface.");
330
            } else {
331 3
                $secretKey = $identity->getMfaSecretKey();
332
333 3
                if (!empty($secretKey)) {
334 3
                    return $this->getOtp()->getQrCodeUri($secretKey, $params);
335
                }
336
            }
337
        }
338
339 1
        return null;
340
    }
341
342
    /**
343
     * Redirects the user browser to the mfa verify page..
344
     *
345
     * Make sure you set [[verifyUrl]] so that the user browser can be redirected to the specified verify URL after
346
     * calling this method.
347
     *
348
     * Note that when [[verifyUrl]] is set, calling this method will NOT terminate the application execution.
349
     *
350
     * @return \yii\web\Response the redirection response if [[verifyUrl]] is set
351
     * @throws ForbiddenHttpException
352
     */
353 11
    protected function verifyRequired()
354
    {
355 11
        if ($this->verifyUrl !== null) {
356 11
            $verifyUrl = (array)$this->verifyUrl;
357
358 11
            if ($verifyUrl[0] !== Yii::$app->requestedRoute) {
359 11
                return Yii::$app->getResponse()->redirect($this->verifyUrl);
360
            }
361
        }
362
363
        throw new ForbiddenHttpException(Yii::t('app', 'Mfa verify required!'));
364
    }
365
366
}
367