I have been trying to create a CakePHP app that will allow me to authenticate in multiple ways. For instance, I want to authenticate using a native account (username/password), Facebook Connect, Google Friend Connect, Twitter Oauth, etc. To start simple, I am just going to separate the user’s account from the user authentication. By default, CakePHP’s AuthComponent wants the user account details in the same table as the user’s authentication details. Something like:
CREATE TABLE users (
id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password CHAR(40) NOT NULL,
created DATETIME,
modified DATETIME
);
But what I want is something more like:
CREATE TABLE users (
id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
created DATETIME,
modified DATETIME
);
CREATE TABLE native_accounts (
id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password CHAR(40) NOT NULL,
user_id INT(11) NOT NULL,
created DATETIME,
modified DATETIME
);
Then, I want to be able to authenticate against the native_accounts table, but set the $this->Auth->user(); to be the user instance that is associated with the native account. There are ways to change which model that the AuthComponent uses, but if I set the model to be my NativeAccount model, then the $this->Auth->user(); will return the NativeAccount that was authenticated, and not the User that is associated with it.
There are other authentication components out there (like the fantastic Authsome component), but I wanted to try and make this work with the standard AuthComponent. So, here is how I did it.
First I created an app_controller:
class AppController extends Controller {
var $components = array('Auth', 'Session');
var $helpers = array('Html', 'Form', 'Session');
function beforeFilter() {
parent::beforeFilter();
$this->Auth->authorize = 'controller';
$this->Auth->allow('user_login');
$this->Auth->fields = array('username' => 'id', 'password' => 'id');
$this->Auth->loginAction = array('controller' => 'my_app', 'action' => 'login');
$this->Auth->logoutRedirect = array('controller' => 'my_app', 'action' => 'logout');
$this->Auth->loginRedirect = array('controller' => 'my_app', 'action' => 'index');
}
function isAuthorized() {
return true;
}
}
This will enable the Auth component for all controllers, set the authorize method to be the controller (which currently allows all logged in users to access all content), allows the “user_login” action (which I will get to in a second), and specifies which User fields the AuthComponent should use for the username and password (which I will also get to in a second…bare with me), and sets up the default login/logout actions.
Next I created the MyAppController. This is the controller that will handle the login/logout, and other common features that aren’t really related to any model.
class MyAppController extends AppController {
var $name = 'MyApp';
var $uses = array();
function index() {
// Will redirect to the login page if the user hasn't logged in yet
}
function login() {
// Just show the login form
}
function logout() {
$this->redirect($this->Auth->logout());
}
}
This controller is pretty easy. It will show the login page when needed, and log the user out when requested. The login view for this looks like:
echo $this->Session->flash('auth');
echo $this->Form->create('NativeAccount', array('action' => 'user_login'));
echo $this->Form->inputs(array(
'legend' => __('Login', true),
'username',
'password'
));
echo $this->Form->end('Login');
For now, it only allows the user to log in using a native account, but when I get around to implementing the other forms of authentication, it will present the user with the various way they can log in.
Finally, the NativeAccountsController looks like:
class NativeAccountsController extends AppController {
var $name = 'NativeAccounts';
function user_login() {
$this->Auth->logout();
if (!empty($this->data)) {
$result = $this->NativeAccount->findByUsername($this->data['NativeAccount']['username']);
if(!empty($result)) {
$hashedPassword = $this->Auth->password($this->data['NativeAccount']['password']);
if ($hashedPassword == $result['NativeAccount']['password'] && !empty($result['User'])) {
$this->Auth->login($result['User']);
} else {
$this->Session->setFlash(__('The password entered did not match the native account\'s', true));
}
} else {
$this->Session->setFlash(__('The native account could not be found', true));
}
}
$this->redirect($this->Auth->redirect());
}
}
The user_login function will try to find the NativeAccount for the given username/password. Keep in mind we need to hash the password ourself because the AuthComponent will only hash the password field for the model that is set to use, which is the User model and not the NativeAccount model. If we are able to find a native account, then we will tell the AuthComponent to login using the user account associated with the native account. When you do this, the AuthComponent will try to find an entry in the User table to validate that it is a real account, and since we the username and password fields of the AuthComponent to be the id, it will try to find the account that has the same id as the one we try to login with, which should exist. If we didn’t set the username and password fields to be the id, then it would try to find a user account using username and password columns which don’t exist on the users table.
So, that’s it. You can then add new tables/controllers to handle the different forms of authentication and then update the login view to offer the different login choices. Users can even setup multiple authentication methods, and log into the same user account in different ways.
There is one final thing to keep in mind. Savvy users could create a form which will post User data to the login action of the MyAppController, and then the AuthComponent will try to authenticate using that data. But this should not be a problem because the AuthComponent will hash the password (which we set to be the id). So, the only way someone could fake a login would be to create a request that has an id that will hash to a valid user id. If you are using the default hash (SHA1), anything id that is sent should result in a 40 character string with letters and numbers after being hashed…and this should never result in matching a user id (which is an int).