I'm working on an ldap authentication module as part of the ldap project for drupal 7. Since other external authentication modules need to migrate to 7 and this functionality is critical to security, I wanted to get some feedback on d7 workflow, hooks and functions related to authentication.

Please let me know if there is other good discussion and resources on this besides api.drupal.org, http://api.drupal.org/api/drupal/modules--user--user.api.php/7, the drupal core issue queue, and http://drupal.org/update/modules/6/7 Or any d7 code from other external auth modules. If we get a good discussion here, I'm willing to write up the results in a sample module for http://drupal.org/project/examples or some documentation in http://drupal.org/developing/modules

I see at least two types of external authentication workflow:

1. Credentials entered in drupal: enter credentials on drupal site, module verifies, and logs user in.
2. Externally entered credentials: drupal login based on cookie, webserver, token etc such as kerberos, webserver authentication, open id.

I'm interested in case 1. for the ldap_authentication module. Below are my initial notes/psuedo code on workflow that I'm looking for feedback on.


/**
 * Implementation of hook_form_FORM_ID_alter().
 */

function ldap_authentication_form_user_register_alter(&$form, $form_state) {
  /**
   * - remove any fields from form that are not appropriate if user is authmaped to
   *    this module.  such as password, email address, username.  Perhaps
   *    offer directions on external site for password change, help etc.
   */
}


/**
 * Implementation of hook_form_FORM_ID_alter().
 *
 * alter both user_login block and page forms
 */

function ldap_authentication_form_user_login_block_alter(&$form, $form_state) {
  ldap_authentication_form_user_login_alter($form, $form_state);
}

function ldap_authentication_form_user_login_alter(&$form, $form_state) {

 /**
  *  sort, add, and remove from  #validate array
  *
  *  user_login_name_validate() (always  called first)
  *  
  *  user_login_authenticate_validate() (remove if only using external auth; not mixed mode)
  *  ldap_authentication_login_form_validate()  (may be before or after user_login_authenticate_validate)
  *  user_login_final_validate() (always last, checks for flood control)
  *
  *  
  */

 
  /**
   * sort, add, and remove from #submit array
   *
   *  ldap_authentication_login_form_submit() must come before user_login_submit
   *    in order to create users and enter in authmap.  Otherwise
   *    we will not be able to leverage standard user login flow.
   *    
   *  user_login_submit() function finalizes login process. user_login_submit
   *    will call user_login_finalize and user_load
   *    which is where we will use hooks to add any ldap attibutes to user data/object
   */  

}


function ldap_authentication_login_form_validate($values) {
  
  /**  will be called between  user_login_name_validate() and user_login_final_validate() **/
  
   /** errors scenarios:
    * can't bind to ldap server with service account.
    * can't find user via ldap query with binding credentials
    * can't find user via ldap query with user entered credentials
    * invoking hook_ldap_authentication_valid_user() returns false with message
    * credentials fail test
    *
    * set form error. ends login process?
    */

}


function ldap_authentication_login_form_submit() {
  /** user has successfully validated and credentials succeeded.
   * if user authenticated via local drupal user account, exit
   *
   * otherwise see if account needs to be created and login
   * this can be done with one function call:
   *  user_external_login_register($drupal_user_name, 'ldap_authentication');
   *     which will create and login the user
   */

}



/** ====================  VARIOUS D7 login/user HOOKS TO PONDER ============================== **/
  
/**
 * Implements hook_user_login().
 *
 * The user just logged in.
 * 
 */

function ldap_authentication_user_login(&$edit, $account) {
  
}


/**
 * Implements hook_user_presave().
 *  A user account is about to be created or updated.
 */

function ldap_authentication_user_presave(&$edit, $account, $category = NULL) {
  
  /** any ldap data stored in user.data array should be reset here
   * if using ldap user attributes to map to .data array
   */
  
}


/**
 * Implements hook_user_insert().
 *
 * A user account was created.
 * The module should save its custom additions to the user object into the database.
 */

function ldap_authentication_user_insert(&$edit, $account, $category) {
  
}


/**
 *  Implements hook_user_update().
 *  
 *  A user account was updated.
 *  Modules may use this hook to update their user data in a custom storage after a user account has been updated.
 */


function ldap_authentication_user_update($edit, $user, $category) {
  
}

Comments

vectoroc’s picture

Look at bulit-in openid.module. I think it's good example.

johnbarclay’s picture

I looked at openid and user modules. user looks like a closer fit for the workflow since it requires entering credentials.

retsamedoc’s picture

OpenID might be a good example. I've looked through it.

Most people who are going to be using LDAP Authentication will be using it exclusively for logins (other than uid=1). This is where the rub is.

OpenID is geared towards an optional authentication scheme.

netw3rker’s picture

there are a lot of SAAS implementations of drupal that offer multiple layers of authentication to systems. I know of one app in particular that has an internal PKI based authentication scheme (for local admin users) then a local LDAP server for special external accounts, then an optional layer for their primary users base that is either CAS/LDAP/anything else. I think we'll find situations like that to be more common than we think. esp, if we build our authentication structure to be accommodating of that.

we shouldn't assume that our module will be run in exclusive use even if a reasonable majority of the time it will be.

netw3rker’s picture

commenting about a comment is kinda fun ;).

function ldap_authentication_form_user_register_alter(&$form, $form_state) {
  /**
   * - remove any fields from form that are not appropriate if user is authmaped to
   *    this module.  such as password, email address, username.  Perhaps
   *    offer directions on external site for password change, help etc.
   */
}

I'm a big fan of allowing users to change their username & email (assuming the admin allows users to based on the core 'allow users to change their username' permission). The current ldap_integration module doesn't allow this because of the requirement that the username match the cn field. This causes all kinds of headaches when dealing with sites that have multiple ldap servers defined and those servers have a set of users that are common to both. (an example from my past: dartmouth.edu and hitchcock.org are a medical school & a hospital. they share about 20% of their accounts because of rotation schedules and various research groups)

If we could avoid changing this form at all & simply make the statement that drupal accounts *map to* ldap accounts (via the authmap table). We'll be playing nicer with drupal's auth system, and in turn making our lives easier. Its not a bad idea to have the email address & username get populated via the ldap module when a new account is created, but after that, if a site admin wants users to change username & password, but still log in with ldap credentials, thats a choice that they should be making outside of our module.

-Chris

netw3rker’s picture

function ldap_authentication_form_user_login_alter(&$form, $form_state) {

/**
  *  sort, add, and remove from  #validate array
  *
  *  user_login_name_validate() (always  called first)
  * 
  *  user_login_authenticate_validate() (remove if only using external auth; not mixed mode)
  *  ldap_authentication_login_form_validate()  (may be before or after user_login_authenticate_validate)
  *  user_login_final_validate() (always last, checks for flood control)
  *
  * 
  */


  /**
   * sort, add, and remove from #submit array
   *
   *  ldap_authentication_login_form_submit() must come before user_login_submit
   *    in order to create users and enter in authmap.  Otherwise
   *    we will not be able to leverage standard user login flow.
   *   
   *  user_login_submit() function finalizes login process. user_login_submit
   *    will call user_login_finalize and user_load
   *    which is where we will use hooks to add any ldap attibutes to user data/object
   */ 

}

This process needs some refinement but is definitely the right way to go. There are at least 2 other conditions here:
1) the choice to authenticate in ldap before local or after local*
2) (rare but possible) something else has already said it is the sole auth system.

* user 1 (and for d7 users with the newly added 'also root' style of permission) should be tested for in our submit/login hook and bypass/short circuit to the local auth system.

In an ideal world every auth module should look for user_login_authenticate_validate in the #validate array & either prepend, append, or replace based on that functionality. If the replace case happens, that should be an indicator to other modules that something has taken over & doesn't want to give up control.

as for this:

   *  ldap_authentication_login_form_submit() must come before user_login_submit
   *    in order to create users and enter in authmap.  Otherwise
   *    we will not be able to leverage standard user login flow.

I'm not sure why, but I'm sort of under the impression that the user creation is actually supposed to happen in the validate hook, thus no need for creation in the submit context. The user_login_validate (and associated validate handlers) are supposed to check for the existence of the global $user->uid and if its defined, do nothing (meaning the user is logged in). This allows us to in our validate handler, upon successful authentication create a user (with authmap record defined) and apply that to the global $user. Once that takes place any other login callbacks should do nothing as a result. I think the #submit sequence is just a formality to ensure that everything knows that a user was successfully logged in (somehow). I could be wrong about this though.

-Chris

johnbarclay’s picture

This worked out pretty well. The user_external_login_register function turned out to be useless since it can't deal with user->data or even the user->mail attribute. Some tweaks in drupal 8 will make it more useful and encourage external authentication modules to set authmaps correctly.

Still looking for feedback. Here is the workflow:
http://www.gliffy.com/publish/2362004/

And the module is at http://drupal.org/project/ldap