Multi Factor Authentication with YII2-mfa plugin

The Problem is, it looks like the script is skipping the login entirely. There is no login, but no view appears with the entry of the otp. The script tries to access the dashboard without logging in. However, this generates errors because the Yii::$app->user->identity is empty.

Is there an example somewhere how to implement yii-mfa in the yii user login?

Without details on how you did things (relevant code) it is hard to help.
But did you do this?

When use it, your identity class must be implementing vxm\mfa\IdentityInterface

And this one:

Notice: when use this widget ensure user had been logged in, if not an yii\base\InvalidCallException will be throw.

Yes. I do this all.

I use the IdentityInterface User model how the tutorial shows:

<?php

namespace app\models;

use Yii;
use app\models\Customer;
use app\models\Usergroups;
use yii\helpers\ArrayHelper;
use yii\db\ActiveRecord;
use vxm\mfa\IdentityInterface;

/**
 * This is the model class for table "user".
 *
 * @property int $id
 * @property string $username
 * @property string $fullname
 * @property string $email
 * @property string $password
 * @property string $last_login
 * @property string $mfa_secret
 */
class User extends ActiveRecord implements IdentityInterface
{

But the script will not login the user. This is my problem.

But i think also that the login after verify the otp should take place.

I think after the user enter his login data the view for the otp-input must show.

But this will not done.

I have also implemented the actions in die SiteController.php

    /**
     * {@inheritdoc}
     */
    public function actions()
    {
        return [
            'error' => [
                'class' => 'yii\web\ErrorAction',
            ],
            'captcha' => [
                'class' => 'yii\captcha\CaptchaAction',
                'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
            ],
            'mfa-verify' => [
                'class' => 'vxm\mfa\VerifyAction',
                'viewFile' => 'mfa-verify', // the name of view file use to render view. If not set an action id will be use, in this case is `mfa-verify`
                'formVar' => 'model', // the name of variable use to parse [[\vxm\mfa\OtpForm]] object to view file.
                'retry' => true, // allow user retry when type wrong otp
                'successCallback' => [$this, 'mfaPassed'], // callable call when user type valid otp if not set [[yii\web\Controller::goBack()]] will be call.
                'invalidCallback' => [$this, 'mfaOtpInvalid'], // callable call when user type wrong otp if not set and property `retry` is false [[yii\web\User::loginRequired()]] will be call, it should be use for set flash notice to user.
                'retry' => true, // allow user retry when type wrong otp
            ]
        ];
    }

And i have also created the mfa-verify view in the site folder.

It seems you missed the note at the bottom of their Github page

Notice: when use this widget ensure user had been logged in, if not an yii\base\InvalidCallException will be throw.

I understand that to mean it works only after normal login. And that is logical. Because the first factor of authentication is normally login with username/password and then verify with some second factor

Ok. I understand this.

I have added a field named mfa_secret to my user table in the database. Because they will registered in User Model with

@property string $mfa_secret

You seen this in the code before. If i added data in this field, the script will not login the user. If the field is empty, the script logged in the user.

In the LoginForm.php (Model) i called

 Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);

to login the user. The getUser()-Functions is

    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = User::findByUsername($this->username);
        } 
        if (empty($this->_user)==true) {
            $this->_user = User::findByEmail($this->username);
        }
        return $this->_user;
    }

It works when the field mfa_secret is empty. But it not works when there is data in the field.

Your login should be working without the OTP stuffs. Make sure it works first.
Then add the method as required by the extension. Adding mfa_secret to database and making it nullable is not a bad idea. remember to add validation rules. BUT, do not use that key anywhere in your login except in the method you override in Identity.

then attach action to the controller where you want to verify as stated in that extension.

That is how I understood it. I have never used it so you need to test things out.

This project uses the extension, you can learn more:

Thank you very much. I’ve do that like the example. But it don’t works. If the field mfa_secret is empty, the login works. But the view with the OTP Input will not show.

Have you another idea for an multi factor authentication?

The best I would advise is to take Yii Basic template, create a small app demonstrating your problem that someone can download and test. That way I or someone else interested to help can download and help. You can upload it to github and include database migration or SQL script. A very simple and straight to the problem.

Other than that I don’t think I have further ideas

Thank you. i am a step go. The program will now load the view. But i have the error because the user is not login. I must look now what the problem is.

That’s very strange. Once it worked and the view for OTP input was shown.

I then logged in again and the view was no longer displayed.

I have no idea what the problem is.

When you run out of ideas do as I suggested and post link to github. I will be happy to have a look at it!

I’m Sorry. I cannot publish the source code because my employer owns the rights.

You missed it. Am not saying you publish your sources.
I was suggesting you take an empty Yii project and add relevant code and database to Yii MFA to create a minima working sample that is not working. Then publish that on github so that someone can look at the problem you are having. The problem is, no one is really going to do that for you, but am sure many might look at the simplified code that can be tested on their machines and try to figure out what the issue is.

No actual code from your project is needed. And since Yii and MFA extension are in public domain, then you shouldn’t have an issue with employer at all. Alternatively the company have to pay a Consultant that will sign NDA and have him look at the actual source code. You can advertise that in Jobs!

This is too much of a hassle for me. I do not have time for that. Sorry.

I have now analyzed the code. The Problem is the login function. They will be called in User-Class in \vendor\yiisoft\yii2\web folder:

    public function login(IdentityInterface $identity, $duration = 0)
    {
        if ($this->beforeLogin($identity, false, $duration)) {
            $this->switchIdentity($identity, $duration);
            $id = $identity->getId();
            $ip = Yii::$app->getRequest()->getUserIP();
            if ($this->enableSession) {
                $log = "User '$id' logged in from $ip with duration $duration.";
            } else {
                $log = "User '$id' logged in from $ip. Session not enabled.";
            }

            $this->regenerateCsrfToken();

            Yii::info($log, __METHOD__);
            $this->afterLogin($identity, false, $duration);
        }

        return !$this->getIsGuest();
    }

In this function the beforeLogin function will be called:

    protected function beforeLogin($identity, $cookieBased, $duration)
    {
        $event = new UserEvent([
            'identity' => $identity,
            'cookieBased' => $cookieBased,
            'duration' => $duration,
        ]);

        $this->trigger(self::EVENT_BEFORE_LOGIN, $event);

        return $event->isValid;
    }

Before the trigger function the field isValid from the Array $event has the value true.

After trigger function is called the field isValid has the value false.

Here is the trigger function (\vendor\yiisoft\yii2\base\Component.php):

public function trigger($name, Event $event = null)
    {
        $this->ensureBehaviors();

        $eventHandlers = [];
        foreach ($this->_eventWildcards as $wildcard => $handlers) {
            if (StringHelper::matchWildcard($wildcard, $name)) {
                $eventHandlers[] = $handlers;
            }
        }
        if (!empty($this->_events[$name])) {
            $eventHandlers[] = $this->_events[$name];
        }

        if (!empty($eventHandlers)) {
            $eventHandlers = call_user_func_array('array_merge', $eventHandlers);
            if ($event === null) {
                $event = new Event();
            }
            if ($event->sender === null) {
                $event->sender = $this;
            }
            $event->handled = false;
            $event->name = $name;
            foreach ($eventHandlers as $handler) {
                $event->data = $handler[1];
                call_user_func($handler[0], $event);
                // stop further handling if the event is handled
                if ($event->handled) {
                    return;
                }
            }
        }

        // invoke class-level attached handlers
        Event::trigger($this, $name, $event);
    }

So if the field isValid is false, the login will not done. This is only so if the field mfa_secret in the database is filled with a string. Otherwise if the mfa_secret field is empty, the login will done and the isValid field is true.

I can only imagine that it may be due to the rules for the fields used in the model user:

    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['username', 'email', 'password', 'customer'], 'required', 'on' => self::SCENARIO_DEFAULT],
            [['username', 'email', 'customer'], 'required', 'on' => self::SCENARIO_USER_UPDATE],
            [['username', 'email'], 'unique'],
            //[['email'], 'unique', 'message' => 'Es existiert bereits ein Benutzer mit dieser E-Mail-Adresse'],
            [['email'], 'email', 'message' => 'Diese E-Mail-Adresse ist nicht korrekt'],
            [['last_login', 'customer', 'active'], 'safe'],
            [['username', 'fullname', 'email', 'mfa_secret'], 'string', 'max' => 255],
            [['firstname', 'lastname'], 'string', 'max' => 100],
            [['password'], 'string', 'max' => 64],
        ];
    }

But I have defined the mfa_secret field only as a string with max 255 characters.

Update: The Problem with the view mfa-verify i have done. I have comment out the call for the view after login. Now the script will view the mfa-verify.

    public function login()
    {
        if ($this->validate()) {

            if(empty($this->username)==true) {
                $this->addError('username', 'Bitte geben Sie ihren Benutzername oder E-Mail ein');
                return false;
            }
            if(empty($this->password)==true) {
                $this->addError('password', 'Bitte geben Sie ihr Passwort ein');
                return false;
            }

            $this->getUser();
            if($this->_user->active==9) {
                $this->addError('password', 'Ihr Benutzer-Account ist zur Zeit inaktiv!');
                return false;
            } else if(Customer::getCustomerStatus($this->_user->customer)<>1) {
                $this->addError('password', 'Ihr Kunden-Account ist zur Zeit inaktiv!');
                return false;
            } else {
                Yii::$app->user->login($this->_user, $this->rememberMe ? 3600*24*30 : 0);
                $this->saveLastLogin();
                //return Yii::$app->response->redirect(Url::base().'/dashboard/')->send();
            }
        }
        return false;
    }

But there is now always the error that the user is not logged in.

Update:

The beforelogin function from the yii2-mfa Behavior.php Script will set the Value $event->isVaild to false when $event->cookieBased is false.

    /**
     * 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.
     *
     * @param UserEvent $event an event triggered
     * @throws ForbiddenHttpException
     */
    public function beforeLogin(UserEvent $event)
    {
        if (!$event->isValid) {
            return;
        }

        if (!$event->identity instanceof IdentityInterface) {
            throw new InvalidValueException("{$this->owner->identityClass}::findIdentity() must return an object implementing \\vxm\\mfa\\IdentityInterface.");
        }

        $secretKey = $event->identity->getMfaSecretKey();

        if (!empty($secretKey) && $this->owner->enableSession && !$event->cookieBased) {
            $event->isValid = false;
            $this->saveIdentityLoggedIn($event->identity, $event->duration);
            $this->verifyRequired();
        }
    }

So i think my login is not Cookie based.

I set the Value enableAutoLogin in vendor/yii-osft/yii2/web/User.php to true (The enableSession is also true setted). But this is not helping.

    /**
     * @var bool whether to enable cookie-based login. Defaults to `false`.
     * Note that this property will be ignored if [[enableSession]] is `false`.
     */
    public $enableAutoLogin = true;

How can i set the Userlogin cookieBased? Any Idea?

if (!empty($secretKey) && $this->owner->enableSession && !$event->cookieBased) {}

Means if secret key is empty and login is not cookie based and session is used to persist login details…

I do not think that is the problem. It must be somewhere else!

see: User, yii\web\User | API Documentation for Yii 2.0 | Yii PHP Framework

Yes. I know this. But the problem is, that the $event->cookieBased is false. The secretKey and $this->owner_enableSession are not empty or false.

So the script will set the $event->isValid to false. And so the application will not login the user.

If i change the script and set the $event->isValid to true, the application logged in the user.

    public function beforeLogin(UserEvent $event)
    {
        if (!$event->isValid) {
            return;
        }

        if (!$event->identity instanceof IdentityInterface) {
            throw new InvalidValueException("{$this->owner->identityClass}::findIdentity() must return an object implementing \\vxm\\mfa\\IdentityInterface.");
        }

        $secretKey = $event->identity->getMfaSecretKey();

        if (!empty($secretKey) && $this->owner->enableSession && !$event->cookieBased) {
            $event->isValid = true;
            $this->saveIdentityLoggedIn($event->identity, $event->duration);
            $this->verifyRequired();
        }
    }

But then i have another error with the QrCodeWidget.

I look now at this error and hope that i will found a solution.

Now i use https://github.com/promocat/yii2-twofa

This works fine.

1 Like

Glad you solved it.
Checked extension it was last updated in 2018.
You might want to use updated fork (March, 2022) https://github.com/pgrond/yii2-twofa