CakePHP 2.x Auth with Two Separate Logins

Back in May, I posted this question. I'm trying to do the same thing again on a different app, but I haven't found a solution to this problem. I do have more information and better code, so I'm hoping you guys can help me sort this out.

Use Case: Doctor's office has a website with admin users. The users login successfully with CakePHP's Auth via User model and UsersController.

Doctors have referring physicians with completely different profiles and actions. Doctors need to login via example.com/physicians/login. However, this login is failing with this

authError => 'You are not authorized to access that location.'

Here is my code in AppController:

class AppController extends Controller {
public $helpers = array('Form', 'Html', 'Time', 'Session', 'Js' => array('Jquery')); 

public $components = array(
    'Session',
    'Auth' => array(
        'autoRedirect' => false,
        'authorize' => 'Controller'
    )
);

public function beforeFilter() {
    $this->Auth->allow('index', 'view', 'edit', 'display', 'featured', 'events', 'contact', 'signup', 'search', 'view_category', 'view_archive', 'addComment', 'schedule', 'login');
}

}

And here is my UsersController that is working:

class UsersController extends AppController {

public $components = array(
    'Auth' => array(
        'authenticate' => array(
            'Form' => array(
                'userModel' => 'User',
                'fields' => array(
                    'username' => 'username',
                    'password' => 'password'
                )
            )
        ),
        'loginRedirect' => array('controller' => 'users', 'action' => 'admin'),
        'logoutRedirect' => array('controller' => 'pages', 'action' => 'index'),
        'loginAction' => array('controller' => 'users', 'action' => 'login'),
        'sessionKey' => 'Admin'
    )
);


public function beforeFilter() {
    parent::beforeFilter();
    $this->Auth->allow('add', 'login', 'logout');
}

function isAuthorized() {
    return true;
}

public function login() {
    if ($this->request->is('post')) {
        if ($this->Auth->login()) {
            $this->redirect($this->Auth->redirect());
        } else {
            $this->Session->setFlash(__('Invalid username or password, try again'));
        }
    }
}

public function logout() {
    $this->Session->destroy();
    $this->redirect($this->Auth->logout());
}

Here is my PhysiciansController code that is NOT working:

class PhysiciansController extends AppController {

public $components = array(
    'Auth' => array(
        'authenticate' => array(
            'Form' => array(
                'userModel' => 'Physician',
                'fields' => array(
                    'username' => 'username',
                    'password' => 'password'
                )
            )
        ),
        'loginRedirect' => array('controller' => 'physicians', 'action' => 'dashboard'),
        'logoutRedirect' => array('controller' => 'pages', 'action' => 'index'),
        'loginAction' => array('controller' => 'physicians', 'action' => 'login'),
        'sessionKey' => 'Physician'
    )
);

public function beforeFilter() {
    parent::beforeFilter();

    $this->Auth->authorize = array(
        'Actions' => array(
            'userModel' => 'Physician',
            'actionPath' => 'physicians'
        )
    );

    $this->Auth->allow('login', 'logout');
// $this->Session->write('Auth.redirect','/physicians/index');
}

function isAuthorized() {   
    return true;    
}   

public function login() {
    if ($this->request->is('post')) {
        if ($this->Auth->login()) {
            $this->redirect(array('controller' => 'physicians', 'action' => 'dashboard'));
        } else {
            $this->Session->read();
            debug($this->Auth);
            $this->Session->setFlash(__('Invalid username or password, try again'));
        }
    }
}

public function logout() {
    $this->Session->destroy();
    $this->redirect($this->Auth->logout());
}

I really don't want to start over and switch to ACL -- I'm not sure that's necessary for just two logins. Help would be very much appreciated!

EDIT: Joshua's answer below is awesome and super helpful. I implemented it, but I'm still receiving an unauthorized error when I try to login as a Physician via /phys/physican/login (prefix/controller/action). The Admin setup works great. Here's the debug code when I try to login:

object(AuthComponent) {
    components => array(
    (int) 0 => 'Session',
    (int) 1 => 'RequestHandler'
)
authenticate => array(
    'Form' => array(
        'userModel' => 'Physician'
    )
)
authorize => false
ajaxLogin => null
flash => array(
    'element' => 'default',
    'key' => 'auth',
    'params' => array()
)
loginAction => array(
    'controller' => 'physicians',
    'action' => 'phys_login'
)
loginRedirect => null
logoutRedirect => '/'
authError => 'You are not authorized to access that location.'
allowedActions => array()
request => object(CakeRequest) {
    params => array(
        'prefix' => '*****',
        'plugin' => null,
        'controller' => 'physicians',
        'action' => 'phys_login',
        'named' => array(),
        'pass' => array(),
        'phys' => true,
        '_Token' => array(
            'key' => 'ad1ea69c3b2c7b9e833bbda03ef18b04079b23c3',
            'unlockedFields' => array()
        ),
        'isAjax' => false
    )
    data => array(
        'Physician' => array(
            'password' => '*****',
            'username' => 'deewilcox'
        )
    )
    query => array()
    url => 'phys/physicians/login'
    base => ''
    webroot => '/'
    here => '/phys/physicians/login'
}
response => object(CakeResponse) {

}
settings => array()

}

Answers


OK I've got a way to do it. You know about prefix routing? If not, read my answer here: CakePHP/MVC Admin functions placement That answer describes how to set up a single routing prefix ('admin'). But you can have any number - just like this:

Configure::write('Routing.prefixes', array('admin','phys','member','user'));
// now we have admin, phys, member and user prefix routing enabled.

What you can do is have all the doctor's methods use 'admin' prefix routing, and all the physicians methods use 'phys' prefix routing.

So the below is code I've hacked together pretty quickly, so it might not be perfect but it should show the concept. Here it is in pseudo code for the before filter method of your app controller:

if (USER IS TRYING TO ACCESS AN ADMIN PREFIXED METHOD) {
    Then use the users table for auth stuff
} else if (USER IS TRYING TO ACCESS A PHYS PREFIXED METHOD) {
    Then use the physicians table for auth stuff
} else {
    It's neither an admin method, not a physicians method. So just always allow access. Or always deny access - depending on your site
}

Here's my app controller code:

App::uses('Controller', 'Controller');

class AppController extends Controller {

    public $components = array('Security','Cookie','Session','Auth','RequestHandler');
    public $helpers = array('Cache','Html','Session','Form');

    function beforeFilter() {

        if ($this->request->prefix == 'admin') {
            $this->layout = 'admin';
            // Specify which controller/action handles logging in:
            AuthComponent::$sessionKey = 'Auth.Admin'; // solution from https://stackoverflow.com/questions/10538159/cakephp-auth-component-with-two-models-session
            $this->Auth->loginAction = array('controller'=>'administrators','action'=>'login');
            $this->Auth->loginRedirect = array('controller'=>'some_other_controller','action'=>'index');
            $this->Auth->logoutRedirect = array('controller'=>'administrators','action'=>'login');
            $this->Auth->authenticate = array(
                'Form' => array(
                    'userModel' => 'User',
                )
            );
            $this->Auth->allow('login');

        } else if ($this->request->prefix == 'phys') {
            // Specify which controller/action handles logging in:
            AuthComponent::$sessionKey = 'Auth.Phys'; // solution from https://stackoverflow.com/questions/10538159/cakephp-auth-component-with-two-models-session
            $this->Auth->loginAction = array('controller'=>'users','action'=>'login');
            $this->Auth->logoutRedirect = '/';

            $this->Auth->authenticate = array(
                'Form' => array(
                    'userModel' => 'Physician',
                )
            );
        } else {
            // If we get here, it is neither a 'phys' prefixed method, not an 'admin' prefixed method.
            // So, just allow access to everyone - or, alternatively, you could deny access - $this->Auth->deny();
            $this->Auth->allow();           
        }
    }

    public function isAuthorized($user){
        // You can have various extra checks in here, if needed.
        // We'll just return true though. I'm pretty certain this method has to exist, even if it just returns true.
        return true;
    }

}

Note the lines:

AuthComponent::$sessionKey = 'Auth.Admin'; // solution from https://stackoverflow.com/questions/10538159/cakephp-auth-component-with-two-models-session

and

AuthComponent::$sessionKey = 'Auth.Phys'; // solution from https://stackoverflow.com/questions/10538159/cakephp-auth-component-with-two-models-session

What that does is allows a person to be logged in as both a physician, and an admin, in the one browser, without interfering with each other's session. You may not need it in the live site, but it's certainly handy while testing.

Now, in you're respective controllers, you'll need straight-forward login/logout methods, with the appropriate prefix.

So, for admin prefixing, in your users controller:

public function admin_login() {
    if ($this->request->is('post')) {
        if ($this->Auth->login()) {
            return $this->redirect($this->Auth->redirect());
        } else {
            $this->Session->setFlash(__('Username or password is incorrect'), 'default', array(), 'auth');
        }
    }
}

public function admin_logout() {
    $this->Session->setFlash('Successfully Logged Out');
    $this->redirect($this->Auth->logout());
}

And in your physicians controller:

public function phys_login() {
    if ($this->request->is('post')) {
        if ($this->Auth->login()) {
            return $this->redirect($this->Auth->redirect());
        } else {
            $this->Session->setFlash(__('Username or password is incorrect'), 'default', array(), 'auth');
        }
    }
}

public function phys_logout() {
    $this->Session->setFlash('Successfully Logged Out');
    $this->redirect($this->Auth->logout());
}

Like I said, that all code I hacked together pretty quickly, so it might not work verbatim, but it should show the concept. Let me know if you have any questions.


Instead of

$this->Session->write('Auth.redirect','/physicians/index'); 

you should use

setcookie("keys", value);

Need Your Help

Why does C# .NET need to be compiled in CLR?

.net windows clr

I know JAVA needs to be compiled in an intermediate language because its Virtual Machine can run on every OS. But as much as I know the CLR only runs on Windows. So why does it need a virtual machi...

count concatenate column in mysql

mysql count distinct pivot-table concat

I want do like this, but for another case that not function.