Example site built with Yii2: NFT Viewer

16. User Authentication

By and large, when creating an application with Yii, you will need to manage users and have a method to allow them to log in to protected, members only areas. Fortunately, right out of the box, Yii supports user authentication.

If you are going to be supporting users, I suggest you implement the advanced template as outlined above, as it will have support for users already available.

16.1 Restricting Access to Actions

Getting right to the point, how do we restrict access to specific actions? The answer is we use the behaviors method found in the controllers.

There are two magic symbols that tell the controller to allow or disallow access to specific actions. The '@' symbol is used to indicate that a user must be logged in to access this action. The '?' symbol is used to say this action is available to anyone, specifically it refers to 'guests'.

Let's look at the SiteController.php file generated by the advanced template. There are three actions:

  • actionIndex
  • actionLogin
  • actionLogout

Action IDs

Especially when dealing with authentication, you will see the Yii documentation refer to 'action ids'. The action id is the method name that starts with action with the action part removed and all remaining characters lowercase. Thus, in the case, the 'action id index' refers to 'actionIndex'.

Let's create one more action called actionTest() along with a view, test.php, that just echoes out the word 'Accessible'.

The action would appear as such:

public function actionTest()
{
	return $this->render('test');
}

We now have four actions to allow or restrict. But, actually, we have five as the 'error' action is global and does need to be addressed. If you accidently restrict access to the 'error' action, you end up with an error trying to display an error page if a non-authenticated user tries to access a restricted page.

We therefore want to restrict access to index, logout and test but allow anyone to access login and error actions.

In its simplest form, you can add the following properties to the behavior method and it will accomplish the above:

'access' => [
    'class' => AccessControl::className(),
    'rules' => [
        [
            'actions' => ['login', 'error'],
            'allow' => true,
            'roles' => ['?'],
        ],
        [
	        'actions' => ['logout', 'index', 'test'],
	        'allow' => true,
	        'roles' => ['@'],
        ],
    ],
],

For context, the entire behavior method will now look like this:

public function behaviors()
{
	return [
	    'verbs' => [
	        'class' => VerbFilter::className(),
	        'actions' => [
	            'logout' => ['post'],
	        ],
	    ],
		'access' => [
		    'class' => AccessControl::className(),
		    'rules' => [
		        [
		            'actions' => ['login', 'error'],
		            'allow' => true,
		            'roles' => ['?'],
		        ],
		        [
			        'actions' => ['logout', 'index', 'test'],
			        'allow' => true,
			        'roles' => ['@'],
		        ],
		    ],
		],
	];
}

The access array first specifies the class to use for access control:

'class' => AccessControl::className()

The next attribute of the array is the rules. The 'actions' attribute specifies which actions are referenced by this rule, referring to them by the action ids.

We set the 'allow' attribute to 'true' meaning users that fit this rule are allowed to access these actions. If we set it to false, these users would be restricted from accessing these actions.

The 'roles' attribute is what specifies the rule to be applied. For the 'login' and 'error' actions, we are specifying the role using the magic operator '?'. This means anyone can access these actions.

For the 'logout', 'index', 'test' actions, we specify the role using the magic operator '@'. This means that the user must be logged in and authenticated to access these actions.

16.1.1 The Only Attribute

The 'only' attribute directs that the rules be applied only to the listed actions. Using 'only' we could accomplish the same thing as above with the following code instead:

'access' => [
	'class' => AccessControl::className(),
	'only' => ['logout', 'index', 'test'],
	'rules' => [
		[
			'actions' => [],
			'allow' => true,
			'roles' => ['@'],
		],
	],
],

The code above says apply the login required role to all actions, but only the 'logout', 'index', 'test' actions.

Actions Empty Array

Passing an empty array to the 'actions' attribute causes the rule to apply to all actions in this controller. If the 'roles' attribute is set to '@' then all actions in the controller will only be accessible by logged in users.

16.1.2 The Except Attribute

The 'except' attribute is the opposite of the 'only' attribute. It means to apply the rules to all actions except those listed. We can thus again re-write the access property to accomplish the same thing as such:

'access' => [
	'class' => AccessControl::className(),
	'except' => ['login', 'error'],
	'rules' => [
		[
			'actions' => [],
			'allow' => true,
			'roles' => ['@'],
		],
	],
],

This code says all actions in the controller can only be accessed by logged in users except the 'login', 'error' actions.

16.2 User Identity

A handy feature included in the user management is access to all user data for the current logged in user.

Thus, if you wanted to access the username of the currently logged in user you would do so as such:

Yii::$app->user->identity->username

Or, you want to access the status field for that user in the user table:

Yii::$app->user->identity->status

16.3 isGuest

There is one other identity property that is useful. The following property will evaluate to True if the user viewing the page is NOT logged in:

Yii::$app->user->isGuest

16.4 RBAC

RBAC is an acronym for Role-Based Access Control and is a rather involved subject. Basically, it allows you to assign roles to users and restrict or allow access based on those roles.

Most commonly, you will have the scenario where there are regular users and there are admins. Admins can access everything, but the regular user can only access the user areas.

There is a great deal of documentation on the Yii website regarding implementation of RBAC. However, as this is a quick start guide, I will provide you the most basic RBAC configuration to accomplish the scenario above, that is two roles: admin and user.

16.4.1 The Basics

Fundamentally, RBAC consists of setting up roles that will be used to differentiate access to different parts of the site. There is a concept of hierarchy here as certain roles can have child roles.

For example, if you are implementing two roles, admin and user, admin will have access to everything User has access to. Thus user is a child of admin.

The other part of the puzzle is the actual assigning each user a specific role.

16.4.2 Specify Manager Type

You have two options for managing and configuring RBAC with Yii: PhpManager and DbManager

PhpManager saves all the role definitions and the user assignments in actual files stored under the directory rbac.

DbManager stores the role definitions and assignments in tables in the database.

To get up and running quickly with two roles, Admin and User, we will use the PhpManager method.

16.4.3 Create RBAC Directory

Directly under the common folder, create a new directory called rbac. This directory must be writable by PHP. There will be two files in the directory: items.php to create the roles and assignments.php to assign roles to users.

16.4.4 Configure authManager

Edit the config file under common: /common/config/main.php. In the components array, add the following config array to tell Yii to use PhpManager.

'authManager' => [
    'class' => 'yii\rbac\PhpManager',
    'assignmentFile' => '@common/rbac/assignments.php',
    'itemFile' => '@common/rbac/items.php',
],

Notice that we have specified the path to the items.php and assignments.php file using the @common alias. This allows our RBAC manager to manage to access both the frontend and the backend. You may want to have the signup form on the frontend and the members protected area on the backend. And, in fact, that is the way Yii Advanced Template comes as default.

16.4.5 Create Items File

Strictly speaking, this can be done through console using a migration file. But, there is no reason we cannot just create it being that it is not complex with only two roles. The items file will go in the rbac directory you just created. Here is the content of the items.php file:

<?php
return [
    'guest' => [
        'type' => 1,
        'description' => 'Nobody',
    ],
    'user' => [
        'type' => 1,
        'description' => 'Can use the query UI',
        'children' => [
            'guest',
        ],
    ],
    'admin' => [
        'type' => 1,
        'description' => 'Can do anything including managing users',
        'children' => [
            'user',
        ],
    ],
];

16.4.5.1 The Migration File

For reference, here is the migration file that you can use to create the items.php file:

<?php

use common\models\User;
use yii\db\Schema;
use yii\db\Migration;

class create_roles_for_predefined_users extends Migration
{
    public function up()
    {
        $rbac = Yii::$app->authManager;

        $guest = $rbac->createRole('guest');
        $guest->description = 'Nobody';
        $rbac->add($guest);

        $user = $rbac->createRole('user');
        $user->description = 'Can use the query UI';
        $rbac->add($user);

        $admin = $rbac->createRole('admin');
        $admin->description = 'Can do anything including managing users';
        $rbac->add($admin);

        $rbac->addChild($admin, $user);
        $rbac->addChild($user, $guest);

        $rbac->assign(
            $user,
            User::findOne(['username' => 'test'])->id
        );
        $rbac->assign(
            $admin,
            User::findOne(['username' => 'admin'])->id
        );
    }

    public function down()
    {
        $manager = Yii::$app->authManager;
        $manager->removeAll();
    }

}

This file would be called create_roles_for_predefined_users.php and would go into the migrations directory under console. If you wanted to run it again for some reason to create the items file, you would do so by executing 'php migrations' via terminal in the Yii root.

You will notice there is also a down() function included. If you wanted to wipe the current items file out along with all assignments, you would execute 'php migrations/down', which you will notice calls removeAll() for the authManager object.

16.4.6 Assignment File

The assignment file is simply an array of arrays with the user id (primary key in users table) as the key and the value being an array containing the name of the role.

So, let's say you currently have two users: one admin user and one regular user. We will say the user id of the admin is 1 and the user id of the regular user is 2. We want to assign the role 'admin' to user 1 and the role 'user' to user 2.

Here is what the assignments.php file will look like:

<?php
return [
    2 => [
        'user',
    ],
    1 => [
        'admin',
    ],
];

That is the entire file!

So what about additional users as they sign up? How do they get added to the assignment file?

16.4.6.1 Adding Users to Assignment

As mentioned, the default install of the advanced template comes with user management already functioning.

The signup form is in a view called signup.php and is under the 'site' directory in 'views' on the frontend. The controller responsible for the member signups is /frontend/controllers/SiteController.php. The action that calls the form and processes the form submission is actionSignup() .

The actionSignup() method creates a new model object of SignupForm() . This model file that contains the submission rules and does the actual saving of the new user to the database is /frontend/models/SignupForm.php.

In method called by the controller in the Signup action to create the new user is $model->signup() . This method is in SignupForm.php.

First, this method runs validation on the submitted info. If it passes validation, a new User model object is created. The username and the email are assigned to the attributes and the setPassword method is called, which performs the hashing encryption of the submitted password. Finally, the save() method is called and the new user is created.

We want to assign the new user to the 'user' role. We will modify the signup() method to do this.

First, we create a new authManager object for the role 'user' by adding the following right after validate is called and passes:

$rbac = Yii::$app->authManager;
$userRole = $rbac->getRole('user');

Then, after the save() method is called, we access the just created user id and assign it to the authManager object we just created, like so:

$rbac->assign(

$userRole,

$user->id

);

The entire signup() function will look like this:

public function signup()
  {
      if ($this->validate()) {
        $rbac = Yii::$app->authManager;
        $userRole = $rbac->getRole('user');

        $user = new User();
          $user->username = $this->username;
          $user->email = $this->email;
          $user->setPassword($this->password);
          $user->generateAuthKey();
          if ($user->save()) {
          $rbac->assign(
            $userRole,
            $user->id
          );

            return $user;
          }
      }

      return null;
  }

When calling $user->save( ), the auto-incremented primary key becomes available at $user->id , which we use to assign the role 'user'.

16.4.7 Setting the Access Control

Now, we just need to modify the access config array in the behavior method of any controller we want to apply authentication to.

Let's refer back to section 16.1. In that section, we discovered how to require authentication for specific actions in a controller. In this case, we wanted to authenticate the following actions: 'logout', 'index' and 'test'.

This was accomplished by adding the following config array to the rules array for the access array:

[
	'actions' => ['logout', 'index', 'test'],
	'allow' => true,
	'roles' => ['@'],
],

If we wanted to change this so that it is required to not only be logged in, but also be of the role 'user', we would simply change the '@' to 'user'.

Let's say we want 'logout' and 'index' to retain generic authentication (no role), but we want to require the action 'test' to be accessible only by users assigned to the role 'user'. Here is how we would modify the access array:

'access' => [
    'class' => AccessControl::className(),
    'rules' => [
        [
            'actions' => ['login', 'error'],
            'allow' => true,
            'roles' => ['?'],
        ],
        [
            'actions' => ['logout', 'index'],
            'allow' => true,
            'roles' => ['@'],
        ],
        [
            'actions' => ['test'],
            'allow' => true,
            'roles' => ['user'],
        ],
    ],
],

Now, only users assigned to the role 'user' will be able to access the 'test' action while any other user not assigned that role will not, even if they are logged in. Of course, since the role 'user' is a child of role 'admin', users assigned to the role 'admin' will also be able to access the 'test' action. If you notice line 18 of the items.php file created above in section 16.4.5, you see how user is assigned as a child of admin.