Authentication adapters

Vegas CMF provides two authentication adapters

  • Standard - authenticate user by identity and credential
  • No credential - authenticate user by identity without credential

Installation

To install authentication library add the following code to composer.json

"vegas-cmf/auth" : "dev-master"

and run command:

php composer.phar update

Standard adapter

Standard adapter provides a default method of authentication process, where user authenticates himself using his identity and password. Follow the 9 steps to see how to setup authentication process in your project.

1. Service provider

Create authentication service provider

touch app/services/AuthServiceProvider.php

use Phalcon\DiInterface;
use Phalcon\Mvc\Url as UrlResolver;
use Vegas\DI\ServiceProviderInterface;

class AuthServiceProvider implements ServiceProviderInterface
{
    const SERVICE_NAME = 'auth';

    /**
     * {@inheritdoc}
     */
    public function register(DiInterface $di)
    {
        $di->set(self::SERVICE_NAME, function () use ($di) {
            $adapter = new \Vegas\Security\Authentication\Adapter\Standard($di->get('userPasswordManager'));
            $adapter->setSessionStorage($di->get('sessionManager')->createScope('auth'));
            $auth = new \Vegas\Security\Authentication($adapter);

            return $auth;
        }, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return array(
            SessionManagerServiceProvider::SERVICE_NAME,
            UserPasswordManagerServiceProvider::SERVICE_NAME
        );
    }
}

Authentication requires session manager registered in dependency injection. Create SessionServiceProvider and SessionManagerServiceProvider to enable session management in your application.

touch app/services/SessionServiceProvider.php

use Phalcon\DiInterface;
use Vegas\DI\ServiceProviderInterface;
use Vegas\Session\Adapter\Files as SessionAdapter;

class SessionServiceProvider implements ServiceProviderInterface
{
    const SERVICE_NAME = 'session';

    /**
     * {@inheritdoc}
     */
    public function register(DiInterface $di)
    {
        $config = $di->get('config');
        $di->set(self::SERVICE_NAME, function () use ($config) {
            $sessionAdapter = new SessionAdapter($config->session->toArray());
            if (!$sessionAdapter->isStarted()) {
                $sessionAdapter->start();
            }
            return $sessionAdapter;
        }, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return array();
    }
}

touch app/services/SessionManagerServiceProvider.php

use Phalcon\DiInterface;
use Vegas\DI\ServiceProviderInterface;

class SessionManagerServiceProvider implements ServiceProviderInterface
{
    const SERVICE_NAME = 'sessionManager';

    /**
     * {@inheritdoc}
     */
    public function register(DiInterface $di)
    {
        $di->set(self::SERVICE_NAME, function() use ($di) {
            $session = new Vegas\Session($di->get('session'));

            return $session;
        }, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return array(
            SessionServiceProvider::SERVICE_NAME
        );
    }
}

Add the following code to app/config/config.php file.

'session' => array(
    'cookie_name'   =>  'sid',
    'cookie_lifetime'   =>  36*3600, //day and a half
    'cookie_secure' => 0,
    'cookie_httponly' => 1
)

Authentication requires a password manager, which also have to be registered in dependency injection.

touch app/services/UserPasswordManagerServiceProvider.php

use Phalcon\DiInterface;
use Vegas\DI\ServiceProviderInterface;

class UserPasswordManagerServiceProvider implements ServiceProviderInterface
{
    const SERVICE_NAME = 'userPasswordManager';

    /**
     * {@inheritdoc}
     */
    public function register(DiInterface $di)
    {
        $di->set(self::SERVICE_NAME, '\Vegas\Security\Password\Adapter\Standard', true);
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return array();
    }
}

2. Module

Create an Auth module and its structure.

mkdir -p app/modules/Auth
mkdir -p app/modules/Auth/config
mkdir -p app/modules/Auth/controllers
mkdir -p app/modules/Auth/controllers/frontend
mkdir -p app/modules/Auth/models
mkdir -p app/modules/Auth/services
mkdir -p app/modules/Auth/views

Create a Module.php file to enable module

touch app/modules/Auth/Module.php

namespace Auth;

class Module extends \Vegas\Mvc\ModuleAbstract
{
    public function __construct() {
        $this->namespace = __NAMESPACE__;
        $this->dir = __DIR__;
    }
}

3. Model

Create a database model.

touch app/modules/Auth/models/User.php


Model for Mongo database
namespace Auth\Models;

class User extends CollectionAbstract implements GenericUserInterface
{
    public function getSource()
    {
        return 'vegas_users';
    }

    public function beforeSave()
    {
        if (!empty($this->raw_password)) {
            $this->writeAttribute('password', $this->getDI()->get('userPasswordManager')->encryptPassword($this->raw_password));
            unset($this->raw_password);
        }
    }

    public function getIdentity()
    {
        return $this->readAttribute('email');
    }

    public function getCredential()
    {
        return $this->readAttribute('password');
    }

    public function getAttributes()
    {
        $userData = $this->toArray();
        //remove password from user data
        unset($userData['password']);
        $userData['id'] = $this->getId();

        return $userData;
    }
}
Model for Mysql database
namespace Auth\Models;

class User extends ModelAbstract implements GenericUserInterface
{
    public function getSource()
    {
        return 'vegas_users';
    }

    public function beforeSave()
    {
        if (!empty($this->raw_password)) {
            $this->writeAttribute('password', $this->getDI()->get('userPasswordManager')->encryptPassword($this->raw_password));
            unset($this->raw_password);
        }
    }

    public function getIdentity()
    {
        return $this->readAttribute('email');
    }

    public function getCredential()
    {
        return $this->readAttribute('password');
    }

    public function getAttributes()
    {
        $userData = $this->toArray();
        //remove password from user data
        unset($userData['password']);
        $userData['id'] = $this->getId();

        return $userData;
    }
}

Create a table in your Mysql database

CREATE TABLE vegas_users(
    id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(200) NOT NULL,
    name VARCHAR(50) NOT NULL
);

Model that will be use in authentication process must implements GenericUserInterface interface.

4. Service

Create a service.

touch app/modules/Auth/services/Auth.php

namespace Auth\Services;

use Auth\Models\User;

/**
 * Class Auth
 * @package Auth\Services
 */
class Auth implements \Phalcon\DI\InjectionAwareInterface
{
    use \Vegas\DI\InjectionAwareTrait;

    /**
     * Authenticates user by email and password
     *
     * @param $email
     * @param $password
     * @throws \Vegas\Security\Authentication\Exception\IdentityNotFoundException
     */
    public function login($email, $password)
    {
        $user = User::findFirst(array(array('email' => $email)));
        if (!$user) {
            throw new \Vegas\Security\Authentication\Exception\IdentityNotFoundException();
        }
        $this->di->get('auth')->authenticate($user, $password);
    }

    /**
     * Ends session
     */
    public function logout()
    {
        $this->di->get('auth')->logout();
    }
}

5. Controller

Create a controller.

touch app/modules/Auth/controllers/frontend/AuthController.php

namespace Auth\Controllers\Frontend;

/**
 * Class AuthController
 *
 * @package Auth\Controllers\Frontend
 */
class AuthController extends \Vegas\Mvc\Controller\ControllerAbstract
{
    /**
     * @return \Phalcon\Http\Response|\Phalcon\Http\ResponseInterface
     */
    public function loginAction()
    {
        $this->view->setLayout('login');

        $this->service = $this->serviceManager->getService('auth:auth');
        if ($this->di->get('auth')->isAuthenticated()) {
            return $this->response->redirect(array('for' => 'root'));
        }
        if ($this->request->isPost()) {
            try {
                $email = $this->request->getPost('email');
                $password = $this->request->getPost('password');
                //authenticates user
                $this->service->login($email, $password);

                return $this->response->redirect(array('for' => 'root'));
            } catch (\Vegas\Security\Authentication\Exception $ex) {
                $this->flash->error($this->i18n->_($ex->getMessage()));
            }
        }
    }

    /**
     * @return \Phalcon\Http\Response|\Phalcon\Http\ResponseInterface
     */
    public function logoutAction()
    {
        $this->view->disable();

        $authService = $this->serviceManager->getService('auth:auth');
        $authService->logout();
        return $this->response->redirect(array('for' => 'login'));
    }
}

Controller uses service Auth to authenticate user by email and password.

6. Routes

Creates a static routes to login and logout action.

touch app/modules/Auth/config/routes.php

return array(
    'login' => array(
        'route' => '/login',
        'paths' => array(
            'module' => 'Auth',
            'controller' => 'Frontend\Auth',
            'action' => 'login',

            'auth'  =>  false
        ),
        'type' => 'static'

    ),
    'logout' => array(
        'route' => '/logout',
        'paths' => array(
            'module' => 'Auth',
            'controller' => 'Frontend\Auth',
            'action' => 'logout',

            'auth'  =>  'auth'
        ),
        'type' => 'static'
    )
);

In case of logout route, we set auth key with value auth, so before dispatch this action, SecurityPlugin will check if user is authenticated in scope auth.

7. Security plugin

Create a plugin that will control required authentication, indicated by route rule.

touch app/plugins/SecurityPlugin.php

class SecurityPlugin extends \Vegas\Security\Authentication\EventsManager\Plugin
{

}

Vegas CMF provides a default implementation of this plugin, so we have to only extend an existing class.

To enable SecurityPlugin add the following code to app/config/config.php file.

'plugins' => array(
    'security' => array(
        'class' => 'SecurityPlugin',
        'attach' => 'dispatch'
    )
)

SecurityPlugin requires also route configuration, that determines where user should be redirected when required session does not exist.

touch app/modules/Auth/config/config.php

return array(
    'auth'  =>  array(
        //name of authentication scope
        'auth'  =>  array(
            //route name
            'route'    =>  'login'
        )
    )
);

8. Layout and view

Create a layout for login page and login form.

Layout

touch app/layouts/login.volt

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="shortcut icon" href="http://getbootstrap.com/assets/ico/favicon.ico">

    <title>Login into VEGAS</title>
    <link href="http://getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="http://getbootstrap.com/examples/signin/signin.css" rel="stylesheet">

    <!--[if lt IE 9]><script src="http://getbootstrap.com/assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
    <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->

    {{ assets.outputCss() }}
    {{ assets.outputJs() }}
</head>

<body>

<div class="container">
    {{ content() }}
</div>

</body>
</html>

View

touch app/modules/Auth/views/frontend/auth/login.volt

{{ flash.output() }}
<form class="form-signin" role="form" method="POST">
    <h2 class="form-signin-heading">Please sign in</h2>
    <input type="email" class="form-control" placeholder="Email address" name="email" required autofocus>
    <input type="password" class="form-control" placeholder="Password" name="password" required>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>

9. Create a user

Once we have a fully configured authentication in application, we can create an user account, to test login process. The easiest and re-usable way to do this, is create a CLI task.

touch app/tasks/UserTask.php

class UserTask extends \Vegas\Cli\Task
{

    /**
     * Available options
     *
     * @return mixed
     */
    public function setOptions()
    {
        $action = new \Vegas\Cli\Task\Action('create', 'Create user account');

        $option = new \Vegas\Cli\Task\Option('email', 'e', 'User email address');
        $option->setRequired(true);
        $action->addOption($option);

        $option = new \Vegas\Cli\Task\Option('password', 'p', 'User password');
        $option->setRequired(true);
        $action->addOption($option);

        $option = new \Vegas\Cli\Task\Option('name', 'n', 'User name');
        $option->setRequired(true);
        $action->addOption($option);

        $this->addTaskAction($action);
    }

    public function createAction()
    {
        $user = new \Auth\Models\User();
        $user->email = $this->getOption('email');
        $user->raw_password = $this->getOption('password');
        $user->name = $this->getOption('name');

        $user->save();

        $this->putSuccess('User created');
        $this->pubObject($user->toArray());

        $this->putText('Done.');
    }
}

Run the following command to use created task
php cli/cli.php app:user create --email=test@vegas.com --password=p@$$w0rd --name=Test

Now you can try login into your application using email: test@vegas.com and password: p@$$w0rd

No credential adapter

No credential adapter might be useful when we need to have a possibility to login as another user, without knowing his password, for example in administrator panel.
Use the structure from the previous example, and create new service.

touch app/modules/Auth/services/AuthEmail.php

namespace Auth\Services;

use Auth\Models\User;
use Vegas\Security\OAuth\Exception\ServiceNotFoundException;

/**
 * Class AuthEmail
 * @package Auth\Services
 */
class AuthEmail implements \Phalcon\DI\InjectionAwareInterface
{
    use \Vegas\DI\InjectionAwareTrait;

    /**
     * Authenticates user by e-mail address
     *
     * @param $email
     * @return mixed
     * @throws \Vegas\Security\Authentication\Exception\IdentityNotFoundException
     */
    public function authenticateByEmail($email)
    {
        $user = User::findFirst(array(array('email' => $email)));
        if (!$user) {
            throw new \Vegas\Security\Authentication\Exception\IdentityNotFoundException();
        }
        $adapter = new \Vegas\Security\Authentication\Adapter\NoCredential();
        $adapter->setSessionStorage($this->obtainSessionScope());
        $auth = new \Vegas\Security\Authentication($adapter);
        $auth->authenticate($user, null);
        return $auth->getIdentity();
    }

    /**
     * Obtains session scope for authentication
     * It safely checks if the session scope with 'auth' name already exists
     *
     * @return mixed
     */
    private function obtainSessionScope()
    {
        $sessionManager = $this->di->get('sessionManager');
        if (!$sessionManager->scopeExists('auth')) {
            $sessionScope = $sessionManager->createScope('auth');
        } else {
            $sessionScope = $sessionManager->getScope('auth');
        }

        return $sessionScope;
    }
}


Create AuthController for example in Backend scope, to authenticate users by theirs e-mail.

touch app/modules/Auth/controllers/backend/AuthController.php

namespace Auth\Controllers\Backend;

use Vegas\Mvc\Controller\ControllerAbstract;

/**
 * Class AuthController
 * @package Auth\Controllers\Backend
 */
class AuthController extends ControllerAbstract
{
    /**
     * @return \Phalcon\Http\Response|\Phalcon\Http\ResponseInterface
     */
    public function authenticateUserAction()
    {
        $this->view->disable();

        if ($this->request->isPost()) {
            try {
                $service = $this->serviceManager->getService('auth:auth');
                $service->authenticateByEmail($this->request->get('email'));

                return $this->response->redirect(array('for' => 'root'));
            } catch (\Vegas\Security\Authentication\Exception $ex) {
                $this->flash->error($this->i18n->_($ex->getMessage()));
            }
        }
    }

    /**
     * @return \Phalcon\Http\Response|\Phalcon\Http\ResponseInterface
     */
    public function logoutUserAction()
    {
        $this->view->disable();

        $authService = $this->serviceManager->getService('auth:auth');
        $authService->logout();
        return $this->response->redirect(array('for' => 'root'));
    }
}

Add routes to app/modules/Auth/config/routes.php

//....
    'authenticateUser' => array(
        'route' => '/admin/authenticateUser',
        'paths' => array(
            'module' => 'Auth',
            'controller' => 'Backend\Auth',
            'action' => 'authenticateUser',

            'auth'  =>  'authAdmin'
        ),
        'type' => 'static'

    ),
    'logoutUser' => array(
        'route' => '/admin/logoutUser',
        'paths' => array(
            'module' => 'Auth',
            'controller' => 'Backend\Auth',
            'action' => 'logoutUser',

            'auth'  =>  'auth'
        ),
        'type' => 'static'
    )
//...

NoCredential adapter should not be use as public authenticator, that's why authAdmin authentication scope is required by this action.
To authenticate user, go to route /admin/authenticateUser/test@test.com where test@test.com belongs to user who will be authenticated.