Creating a User Authentication System
Since we now have the basics of an admin section, it's probably wise to protect it. To do this, we need to use the power of Agavi's built in user system. Agavi has a rather useful system for this, it uses the AgaviRbacSecurityUser class.
Creating the User Class
- startup()
- login()
- logout()
- getCurrentUser()
class BlogUser extends AgaviRbacSecurityUser
{
public function startup()
{
// call parent
parent::startup();
$reqData = $this->getContext()->getRequest()->getRequestData();
if(!$this->isAuthenticated() && $reqData->hasCookie('autologon')) {
$login = $reqData->getCookie('autologon');
try {
$this->login($login['username'], $login['password'], true);
} catch(AgaviSecurityException $e) {
$response = $this->getContext()->getController()->getGlobalResponse();
// Unset the the login cookie since it didn't work
$response->setCookie('autologon[username]', false);
$response->setCookie('autologon[password]', false);
}
}
}
}
The startup() method is rather short and to the point, it checks if the user is already logged in and if the user has a valid cookie stored for automatically logging in.
public function login($username, $password, $isPasswordHashed = false)
{
$userManager = $this->getContext()->getModel('UserManager');
$user = $userManager->retrieveById($username);
if(!$user->getId()) {
throw new AgaviSecurityException('username error');
}
if(!$isPasswordHashed) {
$password = sha1($password . $user->getSalt());
}
if($password != $user->getPassword()) {
throw new AgaviSecurityException('password error');
}
$this->setAuthenticated(true);
$this->grantRoles($user->getRoles());
$this->user = $user;
}
public function logout()
{
$this->clearCredentials();
$this->setAuthenticated(false);
}
public function getCurrentUser()
{
$id = $this->getAttribute('id', 'org.ditabrain.user');
$user = null;
if ($this->isAuthenticated()) {
$userMan = $this->getContext()->getModel('UserManager');
$user = $userMan->retrieveById($id);
}
return $user;
}
<?php
class BlogUser extends AgaviRbacSecurityUser
{
protected $user;
public function startup()
{
// call parent
parent::startup();
$reqData = $this->getContext()->getRequest()->getRequestData();
if(!$this->isAuthenticated() && $reqData->hasCookie('autologon')) {
$login = $reqData->getCookie('autologon');
try {
$this->login($login['username'], $login['password'], true);
} catch(AgaviSecurityException $e) {
$response = $this->getContext()->getController()->getGlobalResponse();
// Unset the the login cookie since it didn't work
$response->setCookie('autologon[username]', false);
$response->setCookie('autologon[password]', false);
}
}
}
public function login($username, $password, $isPasswordHashed = false)
{
$userManager = $this->getContext()->getModel('UserManager');
$user = $userManager->retrieveById($username);
if(!$user->getId()) {
throw new AgaviSecurityException('username error');
}
if(!$isPasswordHashed) {
$password = sha1($password . $user->getSalt());
}
if($password != $user->getPassword()) {
throw new AgaviSecurityException('password error');
}
$this->setAttribute('id', $user->getId(), 'org.bloggie.user');
$this->setAuthenticated(true);
$this->grantRoles($user->getRoles());
$this->user = $user;
return true;
}
public function logout()
{
$this->clearCredentials();
$this->setAuthenticated(false);
}
public function getCurrentUser()
{
if ($this->isAuthenticated()) {
return $this->user;
}
}
}
?>We
need to add the class to the project, there's two things we need to do for this, add it to
app/config/autoload.xml
<autoload name="BlogUser">%core.lib_dir%/user/BlogUser.class.php</autoload>and app/config/factories.xml which is already set as AgaviRbacSecurityUser so change so it looks like below
<user class="BlogUser" />
Creating the Models
class UserManagerModel extends BlogBaseModel
{
private $users = array(
'chuck' => array(
'id' => 'chuck',
'name' => 'Chuck Norris',
'password' => 'a92b2df2a6863585637ac733044be05032bd1a7b', // roundhouse with salt
'salt' => 'b295d117135a9763da282e7dae73a5ca7d3e5b11', // salt
'roles' => array('admin'),
)
);
public function retrieveById($id)
{
if (isset($this->users[$id])) {
return $this->getContext()->getModel('User', null, array($this->users[$id]));
}
throw new Exception('invalid user specified');
}
}
User
Model
class UserModel extends BlogBaseModel
{
protected $id;
protected $name;
protected $salt;
protected $password;
protected $roles;
protected static $fields = array(
'id',
'name',
'salt',
'password',
'roles',
);
public function __construct( $data )
{
$this->fromArray($data);
}
public function toArray()
{
$retval = array();
foreach(self::$fields as $field) {
$retval[$field] = $this->$field;
}
return $retval;
}
public function fromArray(array $data = array())
{
foreach (self::$fields as $field) {
if (isset($data[$field])) {
$this->$field = $data[$field];
}
}
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->Name;
}
public function getPassword()
{
return $this->password;
}
public function getRoles($accountid = null)
{
return $this->roles;
}
return $this->roles;
}
public function getSalt()
{
return $this->salt;
}
public function setName($value)
{
$this->name = $value;
}
public function setPassword($value)
{
$this->password = $value;
}
public function setRoles($value)
{
$this->roles = $value;
}
public function setSalt($value)
{
$this->salt = $value;
}
}
Login Page
st-macbook:demoapp sth$ agavi view-template-create
[property] Loading /Users/sth/Sites/demoapp/build.properties
[agavi.import] Importing external build file /Users/sth/Sites/demoapp/build.xml
Agavi > project-locate:
[property] Loading /Users/sth/Sites/demoapp/build.properties
Agavi > module-locate:
Module name: Default
[property] Loading /Users/sth/Sites/demoapp/app/modules/Default/build.properties
[property] Unable to find property file: /Users/sth/Sites/demoapp/app/modules/Default/build.properties... skipped
Agavi > action-locate:
Action name: Login
Agavi > view-locate:
View name: Input
Agavi > view-template-create:
[property] Loading /Users/sth/Sites/demoapp/build.properties
[agavi.import] Importing external build file /Users/sth/Sites/demoapp/build.xml
Agavi > project-locate:
[property] Loading /Users/sth/Sites/demoapp/build.properties
Agavi > module-locate:
[property] Loading /Users/sth/Sites/demoapp/app/modules/Default/build.properties
[property] Unable to find property file: /Users/sth/Sites/demoapp/app/modules/Default/build.properties... skipped
Agavi > template-create:
Template extension [php]: RETURN KEY
[copy] Copying 1 file to /Users/sth/Sites/demoapp/app/modules/Default/templates
You
need to do this again for the "Success" view. <div id="box">
<h2><?php echo $t['_title']; ?></h2>
<form id="login" action="<?php echo $ro->gen('login'); ?>" method="post">
<fieldset>
<div class="form-row">
<div class="form-label">
<label for="username">Username</label>
</div>
<div class="form-control">
<input type="text" id="username" name="username" />
</div>
</div>
<div class="form-row">
<div class="form-label">
<label for="password">Password</label>
</div>
<div class="form-control">
<input type="password" id="password" name="password" />
</div>
</div>
<div class="form-row-nolabel">
<input type="checkbox" id="remember" name="remember" value="remember" />
<label for="remember">Stay Signed in for a Week</label>
</div>
</fieldset>
<fieldset>
<div class="form-row-nolabel">
<input type="submit" name="login" value="Log in" />
</div>
</fieldset>
</form>
</div>
Login Action
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
parent="%core.module_dir%/Default/config/validators.xml"
>
<ae:configuration>
<validators method="write">
<validator class="string" required="true" name="user_valid">
<argument>username</argument>
<error>Username error.</error>
<ae:parameters>
<ae:parameter name="min">4</ae:parameter>
</ae:parameters>
</validator>
<validator class="string" required="true" name="password_valid">
<argument>password</argument>
<error>Your password is too short.</error>
<ae:parameters>
<ae:parameter name="min">4</ae:parameter>
</ae:parameters>
</validator>
<validator class="isset" required="false">
<argument>remember</argument>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
<?php
class Default_LoginAction extends BlogDefaultBaseAction
{
/**
* Returns the default view if the action does not serve the request
* method used.
*
* @return mixed <ul>
* <li>A string containing the view name associated
* with this action; or</li>
* <li>An array with two indices: the parent module
* of the view to be executed and the view to be
* executed.</li>
* </ul>
*/
public function getDefaultViewName()
{
return 'Input';
}
public function executeWrite(AgaviRequestDataHolder $rd)
{
try {
$user = $this->getContext()->getUser();
$user->login($rd->getParameter('username'), $rd->getParameter('password'));
return 'Success';
} catch (AgaviSecurityException $e) {
return 'Error';
}
}
}
?>
By now, you could log into the site at "/login" but, it's not very functional. We created two templates for two different views earlier in this chapter, so we should add some code into these views.
<?php
class Default_LoginInputView extends BlogDefaultBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$user = $this->getContext()->getUser();
if($this->getContainer()->hasAttributeNamespace('org.agavi.controller.forwards.login')) {
// we were redirected to the login form by the controller because the requested action required security
// so store the input URL in the session for a redirect after login
$user->setAttribute('redirect', $this->getContext()->getRequest()->getUrl(), 'org.agavi.bloggie.login');
} else {
// clear the redirect URL just to be sure
// but only if request method is "read", i.e. if the login form is served via GET!
$user->removeAttribute('redirect', 'org.agavi.bloggie.login');
}
$this->setupHtml($rd);
$this->setAttribute('_title', 'Login');
}
}
?>
The Default_LoginSuccessView:
<?php
class Default_LoginSuccessView extends BlogDefaultBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$user = $this->getContext()->getUser();
$res = $this->getResponse();
// set the autologon cookie if requested
if($rd->hasParameter('remember')) {
$res->setCookie('autologon[username]', $rd->getParameter('username'), '+1 week');
$res->setCookie('autologon[password]', $user->getCurrentUser()->getPassword(), '+1 week');
}
if($user->hasAttribute('redirect', 'org.agavi.bloggie.login')) {
$this->getResponse()->setRedirect($user->removeAttribute('redirect', 'org.agavi.bloggie.login'));
return;
} else {
$this->getResponse()->setRedirect($this->getContext()->getRouting()->gen('index'));
}
$this->setupHtml($rd);
$this->setAttribute('_title', 'Login');
}
}
?>
Making Things Secure
public function isSecure()
{
return true;
}
The isSecure() method is simply just added to the bottom of any action class you want to protect.
The source code for this chapter is available here: Stage 8 (http://www.agavi.org/guide/stages/stage8.tgz

