How to use the OpenID component with the Auth component

Published on and tagged with authentication  cakephp  component  openid

Please note that the hack described in this article no longer works with CakePHP 1.2.3.8166! See Peter’s comment for a possible solution for the current CakePHP release (1.2.5.x).

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.

29 comments baked

  • Robert Scherer

    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

    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

    @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

    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

    @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

    Hi,

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

    Thank you very much on this!

  • cakebaker

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

  • lboy

    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

    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

    @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

    Thank you, this saved my life.

  • cakebaker

    @Fukumori: You are welcome :)

  • lboynton

    I ran into a problem running this code with the latest CakePHP – it does not log me in even with a successful OpenID authentication. It seems somewhere between 1.2 RC4 and 1.2.1 there was a change in the auth component, affecting the identify() method. Long story short, I don’t think login() is expecting an Auth_OpenID_SuccessResponse object in handleOpenIDResponse(), so it doesn’t actually log the user in.

  • cakebaker

    @lboynton: Thanks for your comment!

    Yes, you are right, this approach no longer works. I tried to find a workaround, but without luck so far…

  • lboynton

    I haven’t found a workaround either as yet. Oh yeah, I am lboy by the way, I forgot I posted on here before!

  • Peter Robinett

    With a little tweaking I got the code to work. In the latest stable version of Cake. Thanks so much for the component!

    The relevant code:

    // app/views/user/login.ctp
    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');

    // app/model/user.php
    public function find($conditions = null, $fields = array(), $order = null, $recursive = null) {	
        if (is_array($conditions) && isset($conditions['User.username'])) {
            $newconditions = array('User.openid' => $conditions['User.username']);
            return parent::find($newconditions, $fields, $order, $recursive);
        }
        return parent::find($conditions, $fields, $order, $recursive);
    }

    // app/controllers/users_controller.php
    private function handleOpenIDResponse($returnTo) {
        /* the OpenID response */
        $response = $this->Openid->getResponse($returnTo);        
        if ($response->status == Auth_OpenID_CANCEL) {
            $this->setMessage('Verification cancelled');
        } elseif ($response->status == Auth_OpenID_FAILURE) {
            $this->setMessage('OpenID verification failed: '.$response->message);
        } elseif ($response->status == Auth_OpenID_SUCCESS) {
            $openid = $response->identity_url;
            // ugly hack to remove trailing slash, since Auth component strips it and we need to match
            if (substr($openid, -1) == '/')
            {
                $openid = substr($openid, 0, -1);
            }
            $data = array('User' => array('openid' => $openid));
    
            /* here create the User if it doesn't already exist */
            $user = $this->User->find('first', array('conditions' => array('User.openid' => $openid)));
            /* create user if it doesn't already exist */
            if ($user === false)
            {
                $this->User->create();
            }
            
            /* array of any user details we requested, if the user returned them */
            $sregResponse = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
            $sreg = $sregResponse->contents();
            if ($sreg)
            {
                if(array_key_exists('fullname', $sreg))
                {
                    $data['User']['name'] = $sreg['fullname'];
                }
                if(array_key_exists('email', $sreg))
                {
                    $data['User']['email'] = $sreg['email'];
                }
            }
            /* save user details */
            $success = $this->User->save($data);
            
            $loggedin = $this->Auth->login($response);
            $this->redirect($this->Auth->redirect());
        }
    }
  • cakebaker

    @Peter: Thanks for sharing this code, though here I wasn’t able to make it work so far…

  • Robin

    Thanks go to cakebaker and Peter to getting this working. Good work guys.

  • cakebaker

    @Robin: You are welcome!

  • Nate Hanna

    I’ve was finally able to get the bugs worked out of “Peter’s comment for a possible solution for the current CakePHP release (1.2.5.x)” on my development app where I am integrating OpenID as an alternative to traditional Authentication. I will share my code in a post on my blog/as a new comment here later. I have one issue however that I cannot seem to figure out. When there is a delay/failure with OpenID it renders a “Continue” button without any layout; is there a way to fix this to be within the site’s layout structure?

  • cakebaker

    @Nate: It is hard coded in the current version :| However, it would make sense for a more flexible approach, though I don’t know yet what the best approach is to implement this feature.

  • Nate Hanna

    @cakebaker … that’s what I thought. I’ll take a look over the next few days to see if I can make it more flexible. If I cannot I know two other well rehearsed CakePHP developers (more than I am) that can probably point us in the right direction.

  • Nate Hanna

    @cakebaker … I was able to get it figured out today (a work around for the hardcoded “Continue”). I’ll try to post a solution within the next few days. I would do it now but it has been a long day; time to rest!

  • Matt
  • cakebaker

    @Matt: Thanks for the link!

  • PelleP

    anyone got any idea what to do when I get

    Warning (2): Cannot modify header information – headers already sent by (output started at /customers/pellepersson.se/pellepersson.se/httpd.www/project/app/controllers/users_controller.php:204) [CORE/cake/libs/controller/components/request_handler.php, line 759]

    and when l look at the source code i find a hidden form with information about my openid? I think the problem might be that hidden form but I can’t understand where it come from and how to get rid of it… please help! :)

  • cakebaker

    @PelleP: The hidden form is generated by the authenticate() method of the OpenID component, and is used to submit OpenID information to the OpenID provider.

    Without any code it is difficult to say what causes the warning, so please provide some code ;-)

  • amir

    i am have this simple problem http://stackoverflow.com/questions/6517313/cakephp-and-openid-problem-in-redirect-back

    and what is solution for “Continue” button finnaly?
    Thanks

  • Ethan Brown

    I was able to solve the “Continue” button problem by modifying the code in OpenidComponent.php (in method showFormWithAutoSubmit). The following code is a little elaborate, but the important thing is that I apply style='display:none' to a <div> surrounding $formHtml:

    $msgCss = join(';',array(
                "margin-left:auto",
                "margin-right:auto",
                "margin-top:60px",
                "text-align:center",
                "width:350px",
                "font-family:'Helvetica Neue', Helvetica, Arial, sans-serif",
                "font-size:20px",
                "font-weight:bold",
                "padding:20px",
                "background-color:#333",
                "color:#ccc",
                "-moz-border-radius:20px",
                "-webkit-border-radius:20px",
                "-khtml-border-radius:20px",
                "border-radius:20px"
            ));
            echo '<!DOCTYPE html><html><head><title>' . __('OpenID Authentication Redirect') . '</title></head>'.
                 "<body onload='document.getElementById(\"".$formId."\").submit()'>".
                "<div style=\"width:100%\">".
                "<div style=\"$msgCss\">Logging in...</div>".
                "</div>".
                "<div style='display:none'>".
                 $formHtml.
                "</div>".
                '</body></html>';

Bake a comment




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

© daniel hofstetter. Licensed under a Creative Commons License