How to use the OpenID component with the Auth component

Published on December 09, 2008 and tagged with authentication  cakephp  component  openid

Some days ago I got asked how you can use the OpenID component together with the Auth component from CakePHP.

As I didn’t knew the answer, I had to experiment a bit. Even though it was a quite frustrating experience (thanks to the strange design of the Auth component and too much automagic), I finally managed to find a “solution”. It is rather a hack, but it seems to work ;-)

Ok, here we go.

The first step is to create the login form. Because the Auth component expects the login credentials to consist of username and password, we have to add a (hidden) field for the password.

<?php
// app/views/users/login.ctp
$session->flash('auth');

echo $form->create('User', array('action' => 'login'));
echo $form->input('username', array('label' => 'OpenID:'));
echo $form->input('password', array('type' => 'hidden'));
echo $form->end('Login');
?>

The next step is to create the UsersController. The login() method is a bit special in this case, it is called three times: when you visit the login form, when you submit the login form, and when you come back from the OpenID provider. The rest of the code should be self-explanatory (if not, please leave a comment).

// app/controllers/users_controller.php
class UsersController extends AppController {
    public $components = array('Auth', 'Openid', 'RequestHandler');
 
    public function beforeFilter() {
        $this->Auth->loginError = 'Login failed';
    }
    
    public function login() { 
        $returnTo = 'http://'.$_SERVER['SERVER_NAME'].'/users/login';
		
        if ($this->RequestHandler->isPost()) {   
    	    $this->makeOpenIDRequest($this->data['User']['username'], $returnTo);
        }
    	
        if ($this->isOpenIDResponse()) {
            $this->handleOpenIDResponse($returnTo);
        }
    }
    
    private function makeOpenIDRequest($openid, $returnTo) {
        try {
            $this->Openid->authenticate($openid, $returnTo, 'http://'.$_SERVER['SERVER_NAME']);
        } catch (Exception $e) {
            // empty
        }
    }
    
    private function isOpenIDResponse() {
        return (count($_GET) > 1);
    }
    
    private function handleOpenIDResponse($returnTo) {
        $response = $this->Openid->getResponse($returnTo);
        $this->Auth->login($response);
        $this->redirect($this->Auth->redirect());
    }
    
    public function logout() {
        $this->redirect($this->Auth->logout());
    }
}

As you can see in the handleOpenIDResponse() method, we pass the OpenID response object to the login method of the Auth component. As the Auth component cannot know how to deal with that object, we have to write some functionality to deal with the response object. For this purpose we have to override the find() method of the User model, as it is the method which is eventually called by the Auth component if you call its login() method. The response object is passed to the find() method in the $conditions array and is available via the strange “`User`.`id`” key. The implementation itself is quite simple: you have to return an array with user data if the login was successful, or an empty array if the login was not successful.

// app/models/user.php
class User extends AppModel {
    public function find($conditions = null, $fields = array(), $order = null, $recursive = null) {	
        if (is_array($conditions) && isset($conditions['`User`.`id`'])) {
            $response = $conditions['`User`.`id`'];
			
            if ($response->status == Auth_OpenID_SUCCESS) {
                return array('User' => array('openid' => $response->identity_url));
            }
			
            return array();
        }

        if (is_array($conditions) && isset($conditions['User.username']) && isset($conditions['User.password'])) {
            return array();
        }
		
        return parent::find($conditions, $fields, $order, $recursive);
    }
}

Especially if you have only an OpenID column in your table you have to hinder the Auth component from performing a find() operation using the non-existing columns “username” and “password”, hence the check in the example whether those conditions are set.

With that, it should now be possible to login with your OpenID into your application protected by the Auth component.

I hope this is useful for some of you!

Update (2008-12-24): Fixing some small issues mentioned by lboy in the comments.

12 comments baked

  • Robert Scherer December 09, 2008 at 18:50

    Hello,

    why don’t you just either

    a) make a new Component, extend the AuthComponent and override / modify login() / identify() or the functionality you need?

    or better

    b) give the AuthComponent a Authentication object via $authenticate?

    Regards
    Robert

  • truster December 09, 2008 at 19:12

    Nice, thank you for this tutorial!
    Little note – it is not necessary call $session->check(’Message.auth’) before $session->flash(’auth’) in view, because this check is already done in SessionHelper::flash() method.

    Btw, I want to second to ideas from Robert – that would make it clear implementation, not a hack.

  • cakebaker December 10, 2008 at 17:47

    @Robert, truster: Thanks for your comments!

    @Robert: a): I considered to extend the AuthComponent, but it would also have been a hack, just on a different level. It would have been like extending a Dog class to create a Cat class ;-)

    And the reason why I didn’t use b) is simple: I don’t know how to use it. If I look at the code, I see the $authenticate property is only used in the hashPasswords() method, and nowhere else…

    @truster: Thanks for the hint about SessionHelper::flash(), I fixed it in the article!

  • Robert Scherer December 10, 2008 at 18:18

    Hello Daniel,

    regarding the extending method: I don’t see it as a hack, as long as you’re not diving in too deep in the concrete implementation of the class. I see it as the AuthComponent providing the basic functionality, which you modify. So I think just overriding the “public” methods like identify() or login() is no big deal.

    Regarding the $authenticate property: you’re absolutely right, it is kinda misleading, as it implies that $authenticate provides the same functionality that $authorize provides. I’d like to know the reason – nate, are you reading? :)

  • cakebaker December 11, 2008 at 17:44

    @Robert: It probably depends on your development philosophy/values whether it is ok for you to extend the Auth component. Personally, I often think in concepts and so it gives me the creeps to see on a conceptual level something like “OpenID authentication component is a username/password authentication component” ;-)

  • Jojo Siao December 12, 2008 at 12:11

    Hi,

    I knew you could do a tutorial on how to integrate this with cakephp’s AuthComponent!

    Thank you very much on this!

  • cakebaker December 13, 2008 at 18:59

    @Jojo: You are welcome! And I hope it is useful for you.

  • lboy December 22, 2008 at 23:43

    Just to point out a minor mistake, you have $openid as the first parameter to makeOpenIDRequest(), but don’t actually use it in the function.

    Thanks for this article though.

    As a side note, it seems I have to put my OpenID in the website textbox, and not the OpenID URL textbox, for it to recognise my OpenID.

  • lboy December 23, 2008 at 02:00

    I have also encountered a problem in the overridden find() function. When the $conditions variable is not an array (for example it could be a string like ‘first’) then it will return the first letter in the string, as the array key equates to 0. So:

    $response = $conditions['`User`.`id`']; // response = 'f' when $conditions = 'first'

    I have currently fixed it by checking if $conditions is an array like so:

    if (is_array($conditions) && isset($conditions['`User`.`id`'])
    {
    ...
    }
    
    if (is_array($conditions) && isset($conditions['User.username']) && isset($conditions['User.password']))
    {
    ...
    }
  • cakebaker December 24, 2008 at 10:58

    @lboy: Thanks for your comments!

    The issues you mentioned are now fixed in the article. Also thanks for the hint about the OpenID field in the comment box, it seems like it is no longer necessary with the current version of wp-openid.

  • Fukumori December 29, 2008 at 17:58

    Thank you, this saved my life.

  • cakebaker January 02, 2009 at 18:25

    @Fukumori: You are welcome :)

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License