[Guide] How to actually separate Frontend (User) and Backend (Admin) on Yii2 Advanced

  1. Problem
  2. The Proper Solution
  3. The End :)
  4. Credits

I am writing this guide because I struggled to find a resource that included ALL of the necessary steps to completely separate the frontend from the backend. After reading guides like Configuring different sessions for backend and frontend in yii-advanced-app and yii2 configuring different sessions for backend and frontend in yii advanced application template, there are still steps missing. It is funny how the 2nd one says it is an extension of the first one's article. So I guess, my wiki guide here is yet a 3rd and more complete extension of both of them. I also found a few StackOverflow questions and blog posts regarding this topic. None of them fully worked, and must be missing some magic sauce!

Tip: For testing, use another browser and it's private browsing feature. I use Chrome as my default browser, and during testing I open Firefox in private browsing mode. This way, I can close the browser and all cookies are gone. I don't have to worry about clearing history, cache, cookies, etc.

Problem

Say you have a 'user' table for frontend and you want an 'admin' table for backend. You think you just have to duplicate /common/models/User.php and name it Admin.php, and change /common/models/LoginForm.php to AdminLoginForm, and update the references to them in the backend. Seems logical, and is only the beginning. What will happen is you login to frontend, then open a new tab and go to backend, you will be logged in as a member from the 'admin' table matching the same ID. User ID 3 will have you logged in as Admin ID 3 in the backend. If you click logout in either of them, you get logged out of both.. You get weird behaviour and it just isn't right. Now obviously, we can't have users login to the frontend and be able to manually navigate to /backend and be logged in.

After following the two guides I mentioned earlier, it still didn't work right. I was able to login individually, but the logout of the frontend gave a "400 Bad Request" error. I read that someone changed the POST of the logout to GET. That is absolutely not necessary, a bad idea, and just is not the correct or proper solution. The reason the logout gave a 400 error, was because of invalid CSRF parameters.

The Proper Solution

I am not going to walk you through installing or setting up the Yii2 Advanced Template. The docs already cover that here. So I am assuming you know how to install it and create the user table.

Let's take it from the top with a brand new Yii2 Advanced template. First setup a Yii2 advanced template and run migrate to generate the 'user' table in the database. The go to your /frontend/web/index.php and click signup, and create yourself a new account. I recommend to use the password your wanting to use for your admin account, so the hash is already done for your admin.

Login to your database (phpMyAdmin or whatever you use to manage it) and duplicate or copy the 'user' table naming it as 'admin'. In phpMyAdmin, you click the user table, then the "operations" tab, and there is a copy database option that will also copy the contents with it.

Now you have an 'admin' table, so go to it. Rename the username to 'admin' or whatever you want your admin username to be. The password is whatever you used to create your user during the frontend signup process.

Duplicate /common/models/User.php and name it Admin.php. Open it up, and change the name of the class from 'User' to 'Admin'. You also need to change the tableName() function to return '{{%admin}}'.

Truncated example of /common/models/Admin.php:
class Admin extends ActiveRecord implements IdentityInterface
{
    public static function tableName()
    {
        return '{{%admin}}';
    }
}

Duplicate /common/models/LoginForm.php and name it AdminLoginForm.php. Open it up, and change the name of the class from 'LoginForm' to 'AdminLoginForm'. At the bottom of the class, there is the getUser() function. It references the User class. Change User::findByUsername($this->username) to Admin::findByUsername($this->username). Save the file.

Truncated example of /common/models/AdminLoginForm.php:
class AdminLoginForm extends Model
{
    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = Admin::findByUsername($this->username);
        }

        return $this->_user;
    }
}

Now you need to update /backend/controllers/SiteController.php to use these new models. You need to change all references of 'User' to 'Admin' and 'LoginForm' to 'AdminLoginForm', in the namespaces and the code!

Truncated example of /backend/controllers/SiteController.php:
namespace backend\controllers;

    use Yii;
    use yii\filters\AccessControl;
    use yii\web\Controller;
    use common\models\AdminLoginForm;
    use yii\filters\VerbFilter;

    class SiteController extends Controller
    {
        public function actionLogin()
        {
            if (!\Yii::$app->user->isGuest) {
                return $this->goHome();
            }

            $model = new AdminLoginForm();
            if ($model->load(Yii::$app->request->post()) && $model->login()) {
                return $this->goBack();
            } else {
                return $this->render('login', [
                    'model' => $model,
                ]);
            }
        }
    }

This was common sense for me. Since the backed didn't have it's own login form or model, I knew I needed to make an Admin model to use the 'admin' table in the database, and AdminLoginForm to use that Admin model (ie: Admin::findByUsername() ).

What wasn't clear, was what to do after that. There are issues with the cookies, session, and CSRF values. If we don't manually specify them in the config, then they try to use the same ones for both frontend and backend. That is why you see the weird behaviour, like being logged into both sides after logging into only one of them and getting an error when you try to logout. So let's setup the config.

To be honest, the Yii documentation is lacking, and skims over the advanced template. Don't get me wrong, they wrote a lot and put a lot of effort into the docs that they do have. I just feel that the extra stuff isn't as greatly covered. Hopefully, it continues to grow and gives us more info on the extra stuff. Right now I feel I just have to Google what I need and hope someone else has already covered it.

These issues have nothing to do with programming knowledge, I have been programming for a long time and in many languages.. This has to do with just understanding Yii2. So don't get frustrated :)

In frontend/config/main.php you need to edit the 'user' component, and add 'session' and 'request' to the list.

Truncated example of /frontend/config/main.php:
'components' => [
        'user' => [
            'identityClass' => 'common\models\User',
            'enableAutoLogin' => true,
            'identityCookie' => [
                'name' => '_frontendUser', // unique for frontend
            ]
        ],
        'session' => [
            'name' => 'PHPFRONTSESSID',
            'savePath' => sys_get_temp_dir(),
        ],
        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => '[RANDOM KEY HERE]',
            'csrfParam' => '_frontendCSRF',
        ],
    ],

YOU MUST REPLACE [RANDOM KEY HERE] WITH A REAL RANDOM KEY!

You can use Random.org Random String Generator to generate a nice cookieValidationKey. I am not sure if numbers in the key are allowed, I just have a random string (upper and lowercase) of 20 characters. The session save path, I have it setup to grab the system's tmp directory. So make sure it is writeable (should be). You can change the savePath to wherever you want, but generally sessions are stored in the system's tmp directory by default (ie: php's sessions using session_start() ). I did stumble across a page showing how to use a database for storing the sessions in Yii2. I don't like using databases for sessions. Unauthorized access to the database could compromise them, and it's a lot of server overhead.

What this does is specifies the class for the user (common/models/User.php), a cookie name that would be different from the backend's cookie name, a session name that would be different from the backend's, a secure cookieValidationKey different from the backend's, and a csrfParam different from the backend's.

For the backend, it's the same thing, only we need unique values and to specify the common/models/Admin.php model.

Truncated example of /backend/config/main.php:
'components' => [
        'user' => [
            'identityClass' => 'common\models\Admin',
            'enableAutoLogin' => true,
            'identityCookie' => [
                'name' => '_backendUser', // unique for backend
            ]
        ],
        'session' => [
            'name' => 'PHPBACKSESSID',
            'savePath' => sys_get_temp_dir(),
        ],
        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => '[DIFFERENT UNIQUE KEY]',
            'csrfParam' => '_backendCSRF',
        ],
    ],

Again, use Random.org Random String Generator to generate the cookieValidationKey. DO NOT use the same key you generated for the frontend! You can see, the user 'identityClass' references the Admin model. Also notice the refences to backend.

As long as you use different values for these, you can have as many differnet systems as you want. You could copy the "frontend" folder, and have admin, staff, members, billing, support, forum, blog. As long as you change the values to be unique in the main config of each one, you can create as many as you need. You would just need to create an alias for each one of them.

The End :)

That's it. Now your frontend and backend are completely separate. You can login to each and not have clashes with the other, or worry about other's being able to access it when they shouldn't. This is not a chincy hack, and the most proper way to do it that I have found.

Credits

I want give special thanks to Serge Postrash @SDKiller. Thanks again for helping me with the config values I was missing.