Authentication extensions

Adding new ways of authenticating users can be a valuable feature for enterprises as well as small operations that want to use more secure authentication than just a normal username and password. Enano allows for full-on extension of its authentication system: not only can data such as the username and password be reused and sent to another server for single sign-ons, you can also create entire new fields in the login window.

Label your plugin

If your plugin uses authentication, you must mark it as such in your !info plugin metadata block. This causes the plugin manager to present an additional warning to the site administrator alerting them to the security implications of authentication plugins.

Below is an example of an !info block noting that it is describing an authentication plugin; note specifically the last line in the block.

/**!info**
{
  "Plugin Name"  : "Yubikey authentication",
  "Plugin URI"   : "http://enanocms.org/plugin/yubikey",
  "Description"  : "Allows authentication to Enano via Yubico's Yubikey, a one-time password device.",
  "Author"       : "Dan Fuhry",
  "Version"      : "1.1.6",
  "Author URI"   : "http://enanocms.org/",
  "Auth plugin"  : true
}
**!*/

Failure to label your plugin as described above will result in it being rejected from the Enano plugin gallery, meaning the Enano project will not distribute it. All the Auth plugin flag does is present one extra message box during plugin installation.

Extending login forms

When extending the login form, you must remember that there are two places where you must add your new form fields: the Javascript login window and the static HTML login interface.

HTML login interface

Just attach to the login_form_html hook and echo out an additional table row. The login form has three columns; the first should have the CSS class row2, and subsequent ones should be row1.

<?php
$plugins->attachHook("login_form_html", "myplugin_inject_html_login();");
 
function myplugin_inject_html_login()
{
  global $lang;
  ?>
  <tr>
    <td class="row2">
      Another field:
    </td>
    <td class="row1" colspan="2">
      <!-- Make sure this is the same name as what you put into userinfo -->
      <input type="text" size="40" name="myplugin_val" />
    </td>
  </tr>
  <?php
}

Javascript interface

You will need to attach at two places: the login_build_form hook where the form is constructed, and the login_build_userinfo hook where this information is packed to be sent off to the server.

Note that at login_build_userinfo, the form has already been destroyed, so unless you have some way of pulling your field's data as soon as it's entered, you will also need to attach to the login_submit_early hook where you will pull the value of your plugin's field and store it for later inclusion in the userinfo object.

addOnloadHook(function()
  {
    load_component('jquery');
 
    // include the "table" object here so we can add a row to it
    attachHook('login_build_form', 'myplugin_login_dlg_hook(table);');
    // store the value
    attachHook('login_submit_early', 'window.myplugin_val = document.getElementById("myplugin_auth_field").value;');
    // add it to userinfo; this is the object that gets serialized and encrypted
    attachHook('login_build_userinfo', 'userinfo.myplugin_val = window.myplugin_val; console.debug(userinfo);');
  });
 
function myplugin_login_dlg_hook(table)
{
  // add our field to the table
  var tr = document.createElement('tr');
  $(tr).append('<td>Another field:</td>');
  $(tr).append('<td><input type="text" id="myplugin_auth_field" size="20" /></td>');
  table.appendChild(tr);
}

Dealing with the incoming data

The next step is to attach to the login process, where you can validate the information in your form field and issue a session key or deny login as appropriate. This is done with the login_process_userdata_json hook, which is a return hook - meaning you must return a value.

  • Return true to report that you have logged the user in yourself, e.g. the information the user provided was sufficient to warrant granting a login. You must call $session->register_session() to issue a session key before you return true.
  • Return an array in the format of array('mode' => 'error', 'error' => 'Error string') to stop the login process and report that there was an error. If the value "error" is in the format of a localized string, it will be sent to $lang->get() and localized automatically.
  • Return any other value to report that authentication should continue "down the chain". This can be used for two-factor authentication, or if your own authentication failed but it is not critical for login to succeed.

$plugins->attachHook('login_process_userdata_json',
    'return myplugin_auth_hook($userinfo, $req["level"], $req["remember"]);');
 
function myplugin_auth_hook($userinfo, $level, $remember)
{
  $myval = $userinfo['myplugin_val'];
  // For the sake of brevity, we'll just pretend that this function
  // fetches a ton of info about the user (user ID, password, etc.).
  $userflags = myplugin_get_user_flags($userinfo['username']);
  $myval_valid = myplugin_validate_login($myval, $userflags['user_id']);
  if ( $myval_valid )
  {
    if ( $userflags['two_factor'] )
    {
      // two-factor authentication: require password as well
      return null;
    }
    else
    {
      // one-factor authentication: succeed
      $session->register_session($userflags['user_id'], $userdata['username'],
          $userflags['password'], $level, $remember);
      return true;
    }
  }
  else
  {
    if ( $userflags['two_factor'] )
    {
      // two-factor, and one of the factors failed, so fail authentication
      return array(
        'mode' => 'error',
        'error' => 'Myplugin token invalid'
      );
    }
    else
    {
      // one-factor; this one failed, allow auth to continue validating other factor (password)
      return null;
    }
  }
}

Prevent password changes

Sometimes single sign-on plugins are password-based but are designed to relocate the user's password elsewhere. If a plug-in does this, you must disable Enano's built-in password changing facility so that the user does not become confused. To disable changing the password, use $session->disable_password_change(). You may optionally specify a URL where the user may change their password and the title of the external password-change page, respectively. This method may be called any time after $session is instanciated; we tried it on session_started.

$plugins->attachHook('session_started', 'myplugin_prevent_password_change();');
 
function myplugin_prevent_password_change()
{
  global $session;
  $session->disable_password_change('http://auth.example.com/myaccount/', 'Example.com Account Management');
}

Modify session key generation

You may also add additional data to the process of generating session keys. This is so that when your upstream authentication data changes - for example, a user changes their domain password - existing logged-in sessions will be invalidated. This is done with the session_key_calc hook. There are three variables you will want to use here: $user_id, $key_pieces, and $sk_mode.

  • $user_id will be the numeric UID of the user.
  • $key_pieces is an array you should push your own value or values to.
  • $sk_mode will be either "generate" or "validate".

Design your validation function so that the data you push to $key_pieces would be different if the user changed any authentication parameter in whatever store your plugin validates authentication against.

If you use this feature and your plugin's User CP module (if it has one) requires re-authentication, you must regenerate the user's session entirely. Call ob_end_clean() and $session->logout(USER_LEVEL_CHPREF) to destroy the user's privileged session which is now invalid. Select and fetch the user's HMAC salted password from the database (table enano_users, column password) Then redirect the user back to the User CP homepage (Special:Preferences).

Example

From the Yubikey plugin.

<?php
$plugins->attachHook('session_key_calc', 'yubikey_sk_calc($user_id, $key_pieces, $sk_mode);');
 
function yubikey_sk_calc($user_id, &$key_pieces, &$sk_mode)
{
  global $db, $session, $paths, $template, $plugins; // Common objects
  // gather the user's yubikeys - if they've been changed
  $q = $db->sql_query('SELECT yubi_uid FROM ' . table_prefix . "yubikey WHERE user_id = $user_id;");
  if ( !$q )
    $db->_die();
 
  while ( $row = $db->fetchrow() )
  {
    $key_pieces[] = $row['yubi_uid'];
  }
}
?>

Categories: (Uncategorized)