GrabDuck

AngularJS and Yii2 Part 2: Authentication - Neat Tutorials Blog

:

Our app comes to life.

angularjs-yii2-authentication

In this part we will cover form submission, form validation (with help from Yii2) and authentication. Here’s the the demo. The username is demo and the password is demo. Download the source code from GitHub.

Configuring Yii2

If you haven’t configured the database connection in common/config/main-local.php, this would be a good time. After that apply the migration with the php yii migrate command. You can add a demo user with this SQL query if you need to.

INSERT INTO `user` (`id`, `username`, `auth_key`, `password_hash`, `password_reset_token`, `email`, `status`, `created_at`, `updated_at`) VALUES
(1, 'demo', 'u4qnlunMrSWqcyitTV06gH5C8ZlAaWar', '$2y$13$dN9ipH0Pc2zLBsDGfIkLOuZDvG0Lv5YACMWCAUIYeCHqNKfw3VbDa', NULL, 'demo@localhost.com', 10, 1428424049, 1428424049);

To make authentication work we need to implement the findIdentityByAccessToken() function if common/models/User.php. To make this tutorial simple we will use the auth_key field from the user table, provided by Yii2, as the acces token for our user.

public static function findIdentityByAccessToken($token, $type = null)
{
    return static::findOne(['auth_key' => $token]);
}

Now let’s configure several components in frontend/config/main.php. We won’t be using cookies for authentication, so we will disable ‘enableSession’ => false, for the ‘user’ component. We also wan’t to receive a 401 response instead of a redirect to the login action so our Angular app would know that access was denied ‘loginUrl’ => null.

'user' => [
    'identityClass' => 'common\models\User',
    'enableSession' => false,
    'loginUrl' => null,
],

We will also configure a JsonParser for the Request component, because we will be receiving form data as JSON.

'request' => [
    'class' => '\yii\web\Request',
    'enableCookieValidation' => false,
    'parsers' => [
        'application/json' => 'yii\web\JsonParser',
    ],
],

Lastly we want pretty URLs (e.g. /api/dashboard).

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
],

ApiController

Our frontend/controllers/ApiController.php will extend from yii\rest\Controller instead of yii\web\Controller.

We will be using HTTP Bearer Authentication for our app. AngularjS will send our login and password to Yii2 and Yii2 will send back an access_token. For all the requests after that we will send the access_token as part of the HTTP header in such format:

Authorization: Bearer u4qnlunMrSWqcyitTV06gH5C8ZlAaWar.

yii2-bearer-auth

To configure the HTTP Bearer Auth method we will make some additions to the behaviors() method of our controller. We only have one action (dashboard) that needs authentication, which is why we will apply the ‘authenticator’ filter only to it.

$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
    'class' => HttpBearerAuth::className(),
    'only' => ['dashboard'],
];

Note that we want to keep the behaviors() configuration from yii\rest\Controller which is why we preserve it in the first line $behaviors = parent::behaviors(); and keep adding to it.

Yii2 is capable of XML and JSON responses for REST APIs, but we will only use JSON this time.

$behaviors['contentNegotiator'] = [
    'class' => ContentNegotiator::className(),
    'formats' => [
        'application/json' => Response::FORMAT_JSON,
    ],
];

We also wan’t to allow access to the dashboard only for authenticated users.

$behaviors['access'] = [
    'class' => AccessControl::className(),
    'only' => ['dashboard'],
    'rules' => [
        [
            'actions' => ['dashboard'],
            'allow' => true,
            'roles' => ['@'],
        ],
    ],
];

Finally we can return our behavior configuration return $behaviors;

actionLogin()

Our login action will look very similar to a standard login action. We will populate the LoginForm model with data from Yii::$app->getRequest()->getBodyParams(). And we also have to validate the form before returning it in case the user submitted an empty form. If authentication was successfull we will send the access_token return [‘access_token’ => Yii::$app->user->identity->getAuthKey()];. This is the final result.

public function actionLogin()
{
    $model = new LoginForm();

    if ($model->load(Yii::$app->getRequest()->getBodyParams(), '') && $model->login()) {
        return ['access_token' => Yii::$app->user->identity->getAuthKey()];
    } else {
        $model->validate();
        return $model;
    }
}

Our dashboard is protected by AccessControl so we only need to provide the data for the view in our dashboard action.

public function actionDashboard()
{
    $response = [
        'username' => Yii::$app->user->identity->username,
        'access_token' => Yii::$app->user->identity->getAuthKey(),
    ];

    return $response;
}

To make the Flash in the contact view we will send the content and the class of the flash. Here is our contact action.

public function actionContact()
{
    $model = new ContactForm();
    if ($model->load(Yii::$app->getRequest()->getBodyParams(), '') && $model->validate()) {
        if ($model->sendEmail(Yii::$app->params['adminEmail'])) {
            $response = [
                'flash' => [
                    'class' => 'success',
                    'message' => 'Thank you for contacting us. We will respond to you as soon as possible.',
                ]
            ];
        } else {
            $response = [
                'flash' => [
                    'class' => 'error',
                    'message' => 'There was an error sending email.',
                ]
            ];
        }
        return $response;
    } else {
        $model->validate();
        return $model;
    }
}

That’s it for our API controller.

AngularJS

App.js

We will make a module that will contain our controllers. Let’s tell Angular about it.

var app = angular.module('app', [
    'ngRoute',          //$routeProvider
    'mgcrea.ngStrap',   //bs-navbar, data-match-route directives
    'controllers'       //Our module frontend/web/js/controllers.js
]);

We need to tell the app which view corresponds to which controller.

app.config(['$routeProvider', '$httpProvider',
    function($routeProvider, $httpProvider) {
        $routeProvider.
            when('/', {
                templateUrl: 'partials/index.html',
            }).
            when('/about', {
                templateUrl: 'partials/about.html'
            }).
            when('/contact', {
                templateUrl: 'partials/contact.html',
                controller: 'ContactController'
            }).
            when('/login', {
                templateUrl: 'partials/login.html',
                controller: 'LoginController'
            }).
            when('/dashboard', {
                templateUrl: 'partials/dashboard.html',
                controller: 'DashboardController'
            }).
            otherwise({
                templateUrl: 'partials/404.html'
            });
        $httpProvider.interceptors.push('authInterceptor');
    }
]);

We also pushed an interceptor called authInterceptor. It will add the access_token to the users requests if the user is logged in. And redirect to the login form in case of a “401 Unauthorized” HTTP status.

app.factory('authInterceptor', function ($q, $window, $location) {
    return {
        request: function (config) {
            if ($window.sessionStorage.access_token) {
                //HttpBearerAuth
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.access_token;
            }
            return config;
        },
        responseError: function (rejection) {
            if (rejection.status === 401) {
                $location.path('/login').replace();
            }
            return $q.reject(rejection);
        }
    };
});

controllers.js

Our controllers module will have a MainController that will have two functions. One will return true or false, depending of whether the user is logged in or not. We will use it to show and hide certain menu items the same way we use Yii::$app->user->isGuest in Yii.

The other function will handle the ng-click event for the Logout menu item.

var controllers = angular.module('controllers', []);

controllers.controller('MainController', ['$scope', '$location', '$window',
    function ($scope, $location, $window) {
        $scope.loggedIn = function() {
            return Boolean($window.sessionStorage.access_token);
        };

        $scope.logout = function () {
            delete $window.sessionStorage.access_token;
            $location.path('/login').replace();
        };
    }
]);

The DashboardController will be very simple. It will request data from ‘api/dashboard’ and push the data into the view.

controllers.controller('DashboardController', ['$scope', '$http',
    function ($scope, $http) {
        $http.get('api/dashboard').success(function (data) {
           $scope.dashboard = data;
        })
    }
]);

The LoginController will have one function login(), that will handle the ng-submit event for the login form. The function makes a POST request to ‘api/login’ and sends the username and password. If the request is successful the received session_token is stored and the user is redirected to the dashboard. In case there is an error (the form data is invalid, or the user doesn’t exist) the error data is pushed into the view, where it will be displayed to the user. Here’s how the error data looks when an empty form is submitted.

[{"field":"username","message":"Username cannot be blank."},{"field":"password","message":"Password cannot be blank."}]

Here’s the code for the LoginController.

controllers.controller('LoginController', ['$scope', '$http', '$window', '$location',
    function($scope, $http, $window, $location) {
        $scope.login = function () {
            $scope.submitted = true;
            $scope.error = {};
            $http.post('api/login', $scope.userModel).success(
                function (data) {
                    $window.sessionStorage.access_token = data.access_token;
                    $location.path('/dashboard').replace();
            }).error(
                function (data) {
                    angular.forEach(data, function (error) {
                        $scope.error[error.field] = error.message;
                    });
                }
            );
        };
    }
]);

The ContactController will be the biggest one in our module. It will have two functions. refreshCaptcha() will handle the ng-click event for the captcha image. It will make a GET request to ‘site/captcha?refresh=1’ to get a different captcha if the currently provided one is not readable. The other function contact() will handle the ng-submit event for the contact form. It will POST the form data to ‘api/contact’ and in case of success it will push the “flash” data to the view. After that the form will be cleared and the captcha will be refreshed. In case of an error it will push the error data to the view.

Views

In order for our MainController to work we need to add a directive to the body tag in our layout in frontend/views/layout/main.php. We will also add new menu items to the navbar.

angularjs-yii2-bootstrap-navbar

<ul class="navbar-nav navbar-right nav">
    <li data-match-route="/$">
        <a href="#/">Home</a>
    </li>
    <li data-match-route="/about">
        <a href="#/about">About</a>
    </li>
    <li data-match-route="/contact">
        <a href="#/contact">Contact</a>
    </li>
    <li data-match-route="/dashboard" ng-show="loggedIn()" class="ng-hide">
        <a href="#/dashboard">Dashboard</a>
    </li>
    <li ng-class="{active:isActive('/logout')}" ng-show="loggedIn()" ng-click="logout()"  class="ng-hide">
        <a href="">Logout</a>
    </li>
    <li data-match-route="/login" ng-hide="loggedIn()">
        <a href="#/login">Login</a>
    </li>
</ul>

Notice the ng-show and ng-hide directives for the last three menu items. They will show or hide menu items depending on the value of the loggedIn() function.

Login form

We need to add several directives to frontend/web/partials/login.html to make the form work. ng-submit=”login()” will tell AngularJS to run the login() function when the form is submitted.

The ng-class directive will help us to set a ‘has-success’ or ‘has-errors’ css class for form inputs depending on whether they have validation errors or not.

angularjs-yii2-bootstrap-form-validation

ng-model=”userModel.username”   binds input field data to a variable.

Finally we will display error messages in the following way

<p class="help-block help-block-error">{{ error['username'] }}</p>

Here’s how our form will look.

<div class="row">
    <div class="col-lg-5">
        <form ng-submit="login()" name="loginForm" id="login-form" method="post" role="form" >
            <div ng-class="{ 'has-success': !error['username'] && submitted,
                'has-error': error['username'] && submitted }"
                 class="form-group field-loginform-username required">
                <label class="control-label" for="loginform-username">Username</label>
                <input ng-model="userModel.username" type="text" id="loginform-username" class="form-control">
                <p class="help-block help-block-error">{{ error['username'] }}</p>
            </div>

            <div ng-class="{ 'has-success': !error['password'] && submitted,
                'has-error': error['password'] && submitted }"
                 class="form-group field-loginform-password required">
                <label class="control-label" for="loginform-password">Password</label>
                <input ng-model="userModel.password" type="password" id="loginform-password" class="form-control">
                <p class="help-block help-block-error">{{ error['password'] }}</p>
            </div>

            <div class="form-group">
                <button type="submit" class="btn btn-primary" name="login-button">Login</button>
            </div>

        </form>
    </div>
</div>

 Contact form

The contact form will be very similar. Except for two parts.

In the beginning we will include a div for the Flash message.

angularjs-yii2-contact-form-flash-message

It will normally be hidden thanks to the ng-show directive and will only appear when the flash variable is pushed to the scope of the view.

<div ng-show="flash" class="alert alert-{{ flash.class }}">
    {{flash.message}}
</div>

The other part that’s different about the contact form is the captcha image. It has a ng-click event and the source for the image is provided by the captchUrl variable.

<img ng-click="refreshCaptcha()" ng-src="{{captchaUrl}}" id="contactform-verifycode-image" alt="">

AppAsset

Let’s add our controllers.js to frontend/assets/AppAsset.php so Yii2 would add it to our layout.

public $js = [
    'js/app.js',
    'js/controllers.js',
];

 Homework

Try adding the signup form yourself. It’s very similar to the login form.

  1. Add a signup action to frontend/controllers/ApiController.php
  2. Add a signup controller to frontend/web/js/controllers.js
  3. Add a signup menu item to frontend/views/layouts/main.php
  4. Add a partial view with the signup form to frontend/web/partials/

Conclusion

Yii2 is great for building RESTful services. It provides three authentication methods HTTP Basic Auth, HTTP Bearer Auth and Query Parameter Auth. They can be used separately or together thanks to  yii\filters\auth\CompositeAuth.

The form validation method implemented in this tutorial is incomplete. It can and should be improved for production grade applications, which is beyond the scope of this tutorial. The AngularJS part of the application can benefit from restructuring. If you wan’t to learn more about building Angular apps you can examine the code of angular-app. Angular apps that have node.js back ends benefit greatly from code sharing (e.g. validation rules), which isn’t possible for PHP back ends.

Yii2 also provides yii\rest\ActiveController for RESTful services, which would be a great topic for a third part.