I'm working on a module to support drupal logins using OpenID. For those of you not familiar, OpenID is a decentralized identity system. In OpenID, a users identity is keyed by a url that represents them. A "Consumer" is the site that the user wants to log in to, i.e. drupal, and the "Server" is the site that knows about the user.
The first problem I encountered is that OpenID users should not enter their password on the Consumer site. This should be a fairly obvious security concern. To side step this problem a created a separate block for OpenID login only that has one field for the user to enter their url. Does anyone see a problem with this?
I'm now stuck trying to figure out what to do about creating user accounts, user names, email address, etc. First of all, It doesn't seem that a URL is a valid drupal username. Ideally, when a user name is displayed for an OpenID user, it would appear as a hyperlinked URL. Second, it appears that an email address is required. I suppose that could be resolved by prompting the user for an email address when they first use their OpenID.
One thought I had was that I could create a separate database table the maps OpenID to user account. That could allow existing users to associate an openid with their account.

If anyone has any input, it would be greatly appreciated.

Comments

chx’s picture

as you might know http://username:password@my.site is a widely used url scheme (despite not valid, the logn part is only for FTP). Omit username:password and get http://@my.site . That is something that Drupal will understand as an external userid.

Caveat: Opera will give you a warning if you click on such a URL. Opera takes security very seriously.
--
Read my developer blog on Drupal4hu.

--
Drupal development: making the world better, one patch at a time. | A bedroom without a teddy is like a face without a smile.

javanaut’s picture

First off, thank you for working on this. I see OpenID as an important contribution that could someday take over Drupal's internal distributed auth system.

To your point, I thought the OpenID Server sent back a handle that you could use to construct a local account with. For example, if you loged in as "bloghost.com/users/brad123" then the server returned a parameter of "Brad".

When I first read the idea of representing users as URLs instead of "user" or "user@host", I was a bit curious how these identities would be integrated into conventional systems. I'm wondering if Drupal's auth mechanism will need to be expanded to accommodate this format.

tnarg’s picture

The way OpenID is defined, it is intended that the URL be the handle. Otherwise, if something like "Brad" was returned, there would be no guarantee that it wouldn't already be used by someone else on the site.

I think that ideally, different authorization mechanisms would have there own tables that pointed into the global user table. This way, things like passwords and identifiers would be associated with a particular authorization mechanism. Then the user table would just contain a display name.

walkah’s picture

we can already make extensive use of the authmap table... yes, i agree that the user@server restriction needs to be lifted (anyone noticed the zcallbacks.module hacking that ldap_integration has had to do??

we may need to augment authmaps slightly, but it's exactly the base we want to build on.

--
James Walker :: Bryght Guy

--
James Walker :: http://walkah.net/

tnarg’s picture

Here is a copy of what I have so far. Currently it just does the OpenID Identity verification and prints out some messages indicating whether it was successful or not. In particular, the "doXXX" methods of the DrupalActionHandler need to actually do something.

<?php
// $Id: openid.module,v 1.9 2005/04/25 16:45:26 unconed Exp $

/**
 *  This module requires that you have version 0.0.8.X PHP-OpenID
 *  library somewhere where php can find it.
 *  The PHP-OpenID library is available at the following url:
 *
 *      http://videntity.org/openid/
 */
require_once( 'PHP-OpenID-0.0.8.3/openid/consumer.php' );


/**
 * Implements hook_help
 */
function openid_help($section) {
  switch ($section) {
  case 'admin/modules#description':
    return t('Allows users to authenticate via OpenID');
  }
}

/**
 * Return info for dist_auth usage
 */
function openid_info($field = 0) {
  $info['name'] = 'OpenID';

  if ($field) {
    return $info[$field];
  }
  else {
    return $info;
  }
}

/**
 * Implement hook_auth to show in available dist_auth list
 */
function openid_auth($username, $password, $server){
  return;
}

/**
 * Implementation of hook_block
 * Provides the OpenID login form
 */
function openid_block($op='list', $delta=0) {
  global $user;
  // listing of blocks, such as on the admin/block page
  if ($op == "list") {
    $block[0]["info"] = t("OpenID Login");
    return $block;
  } else {
    if (!$user->uid) {
      // set up the block
      $block['subject'] = 'OpenID Login';

      $content = "<div class=\"user-login-block\">\n";
      $content .= form_textfield(t('OpenID'), 'identity_url', $edit['identity_u\rl'], 15, 64);
      $content .= form_submit(t('Log in'));
      $content .= "</div>\n";
      $block['content'] = form($content, 'post', url('openid/login'));
      return $block;
    }
  }
}


/**
 * Menu entries
 * openid/login: Handles form submit from openid_block
 * openid/return: handles redirect return from identity server
 *
 */
function openid_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    // OpenID Form submit
    $items[] = array('path' => 'openid/login', 'title' => t('openid login'),
                     'callback' => 'openid_login', 'type' => MENU_CALLBACK,
                     'access' => TRUE);
    $items[] = array('path' => 'openid/return', 'title' => t('openid return'),
                     'callback' => 'openid_ret', 'type' => MENU_CALLBACK,
                     'access' => TRUE);
  }

  return $items;
}


/**
 * Callback for the OpenID login form
 */
function openid_login() {
  $edit = $_POST['edit'];
  $op = $_POST['op'];

  $identity_url = $edit['identity_url'];

  $query = array_merge( $_GET, $_POST );
  $consumer = new DrupalConsumer(new DrupalHTTPClient());
  $handler = new DrupalActionHandler($query, $consumer);

  $ret = $consumer->find_identity_info($identity_url);
  if( !$ret ) {
    printf('Unable to find openid server for identity url %s', $identity_url);
  } else {
    // found identity server info
    list( $identity_url, $server_id, $server_url ) = $ret;

    $trust_root = url(NULL, NULL, NULL, TRUE);

    // build url to application for use in creating return_to
    $app_url = url('openid/return', NULL, NULL, TRUE);

    // create return_to url from app_url
    $return_to = $handler->createReturnTo($app_url, $identity_url);

    // handle the request
    $redirect_url = $consumer->handle_request(
                                              $server_id, $server_url, $return_to, $trust_root);

    // redirect the user-agent to the server
    header( 'Location: ' . $redirect_url );
    exit;
  }
}


/**
 * Callback for return_to url redirection. The Identity server will redirect
 * back to this handler with the results of the authentication attempt.
 */
function openid_ret() {
  $edit = $_POST['edit'];
  $op = $_POST['op'];

  $query = array_merge( $_GET, $_POST );
  $consumer = new DrupalConsumer(new DrupalHTTPClient());
  $handler = new DrupalActionHandler($query, $consumer);
  $openid = $handler->getOpenID();
  $req = new ConsumerRequest($openid, $query, 'GET');
  $response = $consumer->handle_response($req);
  $response->doAction($handler);
}


/**
 * Our OpenIDConsumer subclass.  See openid.consumer.OpenIDConsumer
 * for more more documentation.
 */
class DrupalConsumer extends OpenIDConsumer {

  function verify_return_to( $return_to ) {
    if( strpos($return_to, url('openid/return', NULL, NULL, TRUE)) != 0) {
      return false;
    }
    return true;
  }
};


/**
 * A handler with application specific callback logic.
 */
class DrupalActionHandler extends ActionHandler {

  function DrupalActionHandler($query, $consumer) {
    $this->query = $query;
    $this->consumer = $consumer;
  }

  /* callbacks */
  function doValidLogin($login) {
    // here is where you would do what is necessary to log an openid "user"
    // user into your system.  We just print a message confirming the
    // valid login.
    printf( '<b>Identity verified!</b> Thanks, ' .
            '<a href="/%s">' .
            '%s</a>', $this->query['open_id'], $this->query['open_id'] );
  }

  function doInvalidLogin() {
    print 'OpenID verification failed!';
  }

  function doUserCancelled() {
    print 'OpenID login Cancelled by user.';
  }

  function doCheckAuthRequired($server_url, $return_to, $post_data) {
    // do openid.mode=check_authentication call, and then change state
    $response = $this->consumer->check_auth($server_url, $return_to,
                                            $post_data, $this->getOpenID());
    $response->doAction($this);
  }

  function doErrorFromServer($message) {
    print 'Error from Identity Server: ' . $message;
  }

  /* helpers */
  function createReturnTo($base_url, $identity_url, $args=null) {
    if( !is_array( $args ) ) {
      $args = array();
    }
    $args['open_id'] = $identity_url;
    return oidUtil::append_args($base_url, $args);
  }

  function getOpenID() {
    // return the openid from the original form
    return $this->query['open_id'];
  }
};


/**
 * A wrapper around drupal_http_request presenting the
 * expected interface to the OpenID library
 */
class DrupalHTTPClient extends HTTPClient {

  function get( $url) {
    // -->(final_url, data)
    // Fetch the content of url, following redirects, and return the
    // final url and page data as a tuple.  Return NULL on failure.

    $redirs = 0;
    while ($redirs < 10) {
      $result = drupal_http_request($url, array(), 'GET', NULL, 0);

      switch ($result->code) {
      case 200:
        return array($url, $result->data);
      case 301:
      case 302:
      case 307:
        $url = $result->redirect_url;
        $redirs += 1;
        break;
      default:
        return NULL;
      }
    }
  }

  function post( $url, $body) {
    // -->(final_url, data)
    // Post the body string argument to url.
    // Reutrn the resulting final url and page data as a
    // tuple. Return NULL on failure.
    $result = drupal_http_request($url, array(), 'POST', $body, 0);

    if($result->code != 200) {
      return NULL;
    }
    return array($url, $result->data);
  }

  // static
  function getHTTPClient() {
    return new DrupalHTTPClient();
  }

};

?>
boris mann’s picture

We (Bryght) definitely want to support OpenID, perhaps as a secure replacement for Drupal auth, but in general as one of the main open distributed identity mechanisms.

You might want to check out the SXIP module that James created, as it deals with a lot of these same issues. James has been working with a bunch of Jabber stuff that might be related as well, and is in general an expert on those concepts of user/distributed auth stuff.

Perhaps we can have a meeting over Skype or IRC for everyone interested in identity stuff and Drupal? Use my contact form to get in touch. We should also put this on the agenda for the meetings in Amsterdam.

--
Please turn on the "story" type, so we can use it to have an archive of best practices, how tos, and configuration recipes.

alexmc’s picture

I'm happy to help with the OpenID implementation either in coding, testing or documentation. Just give me a shout when there is some kind of SIG.

It is rather hard to see who is working on this right now.

yuumei’s picture

Hi, I have been messing with the OpenID module and added a setting for assigning "default roles" to an OpenID registered user.
I have changed the following:

Add the roles to the user on signup (line 309):

    $user = user_save('', array('name' => $_SESSION['openid'],
                                'pass' => user_password(),
                                'mail' => $query['edit']['email'],
                                'init' => $_SESSION['openid'],
                                'status' => 1,
                                "authname_openid" => $_SESSION['openid'],
                                'roles' => variable_get('openid_default_roles', DRUPAL_AUTHENTICATED_RID))); // <-- here

Add a function to change the settings:

function openid_settings() {
  // Default role settings:
  $roles = user_roles(1);
  $options = $roles;
  $form['openid'] = array('#type' => 'fieldset', '#title' => 'OpenID settings');
  $form['openid']['openid_default_roles'] = array('#type' => 'checkboxes', '#title' => t('Default roles'), '#default_value' => variable_get('openid_default_roles', ''), '#options' => $options, '#description' => t('Enter the default user roles for an OpenID user.'));

  return system_settings_form('openid_settings', $form);
}

Add the help text:

function openid_help($section) {
    switch ($section) {
    case 'user/help#openid':
        return t('Allows users to authenticate via OpenID');
    case 'admin/modules#description':
      return t('Allows users to authenticate via OpenID');
    case 'admin/settings/openid':
      return t('<p>Here, you can edit the defaults for the OpenID module</p>');

    }
}

Add the menu options:

function openid_menu($may_cache) {
    $items = array();

    $items[] = array('path' => 'openid/get_email', 'title' => t('openid email form'),
                     'callback' => 'openid_email_form', 'type' => MENU_CALLBACK,
                     'access' => TRUE);
    $items[] = array('path' => 'openid/login', 'title' => t('openid login'),
                     'callback' => 'openid_login', 'type' => MENU_CALLBACK,
                     'access' => TRUE);
    $items[] = array('path' => 'openid/return', 'title' => t('openid return'),
                     'callback' => 'openid_return', 'type' => MENU_CALLBACK,
                     'access' => TRUE);
    $items[] = array('path' => 'admin/settings/openid',
                     'title' => t('openid'), 'callback' => 'openid_settings');

    return $items;
}

^^*

shadyman@erroraccessdenied.com’s picture

Cool beans so far

Check out http://iwantmyopenid.org/ - they have bounties going for OpenID support.

henadzi’s picture

Found OpenID News

I think links located there may be useful for openid beginners ...
---
>MyDrupal