This documentation is related to the User Data Connector Module.

Introduction

The User Data Connector Module allows you to perform user authentication and obtaining information about users from a Drupal-external PHP script using a simple and compact API. The module is useful if your script environment (session, error callback, exception callback, shutdown function, defined global variables, defined functions) must not be changed, as it would happen if you bootstrap Drupal and use its functions and classes.

Who/what is this module for?

This is definitely a module for PHP developers and administrators, not for users. It can be interesting for you if you implemented a PHP script that requires authentication or user information, and you want your users to be synchronized with the database of you Drupal installation. The module allows you as well to define user profile fields in Drupal for your external script.

Client API summary

The API you include in your external script has three classes, all located in the directory "<root>/sites/all/modules/udc/client/", where <root> is the Drupal root folder, which should be the $_SERVER['DOCUMENT_ROOT']. The class source code is php/javadoc documented, so that IDEs like Eclipse or NetBeans auto-complete the object methods and instance variables for you, and list method/property information in-situ.

  • The DrupalUserAuth class allows you to check if a login name/password pair matches. This will not affect the Drupal page login state of the user because only a password check using the corresponding Drupal core functions is performed. The return values are stored in the object's properties and include username (correct key case like in database), email, roles, fields (only allowed fields in the module configuration), an 'active' flag that indicates if the user is active or blocked/cancelled, and the "valid" flag that indicates that the password was correct.
  • The DrupalUserInfo class enables you to get detailed information about a user without performing an authentication check. The object properties are equivalent to the properties of DrupalUserAuth.
  • The DrupalUserList class can be used to retrieve a list of users. The only instance variable is "list", which will contain an associative array with the users. Each entry implies name, email and active flag. If you invoke the request method with the corresponding arguments, each entry contains the roles of the user as well.

To provide this functionality, the module consists of two communicating parts: The client class API and the server script. The client classes, which you will use, send JSON requests to the server via HTTPS or HTTP on localhost. The server script bootstraps Drupal (only the necessary part to save time), processes the user requests and sends back the JSON results - or an error message. The client API class parses the response and puts the results in its instance variables.

Module requirements

The implementation of the API and the server requires a PHP version >= 5.2, if you want to enable SSL/HTTPS for the client-server connections, you need PHP with the cURL library. The configuration form shows you if this feature can be enabled or not. There are no special Drupal module dependencies. UDC will ignore the deprecated Profile module if it is disabled, but can handle the data if it is enabled.

As your web server configuration might disallow the execution of scripts that are not located in the document root, you should ensure that the server script (/sites/all/modules/udc/server/server.php) can be accessed.

Security

Using a local network connection is quite often used (e.g. database connections), but of cause it can be dodgy according to the security of the user data. For this reason, the module has some features to prevent getting in trouble, all of them implemented in the server script:

  • The server only responds to requests coming from localhost or its own IP address
  • The server only responds if the module is enabled
  • You can configure the server only to accept HTTPS connections
  • You must specify a security token in the configuration, which is used to identify the client
  • You can exclude single users by name from the response
  • You can specify which user roles are allowed to be sent to the client
  • You can restrict the profile fields that can be sent to the client
  • Neither user UID nor password hash are sent in the response

The token is required because the server script can only check if the request comes from its own IP. If you are the one and only administrator on the server, the system is safe without a token identification. However, normally more than one virtual host are on a web server. Hence, all have the same IP address. Having no additional client identification would allow other web masters on your server to get information about your users. Fortunately, what they do not have is access to your file system and database (if they do they can get your user data anyway). Hence, an identification token prevents external requests from the same server. In case that the request does not come form the server IP, a 404 response will be sent immediately. If any of the scripts is included in normal Drupal context, an exception will be thrown.

The client API cannot check if your external scripts are secure! Here some recommendations:

  • If your script shows a login page or uses the Basic Autenication method for login, ensure that the user is on HTTPS channel. You can do this by checking if $_SERVER['HTTPS'] == 'on' or if the port is 443.
  • Be very careful when implementing a script that repeates the some functionality of the UDC module if this script can be accessed from the internet. One effort of the UDC module is to keep the user data only on the server where it is installed.

Flags and settings in the client API

Here the list of flags and settings that the API will recognize:

  • Constant USER_DATA_CONNECTOR_USE_SSL (bool): You need set this constant to TRUE if you selected "SSL only" on the module configuration page.
  • Constant USER_DATA_CONNECTOR_MODULE_PATH (string): You can use this constant to overwrite the default module path detection. This path is mainly used build the URL to the server script. This can be usefull to prevent connection errors if you use symlinks to your document root ($_SERVER['DOCUMENT_ROOT']). The URL to the server script is build like [http|https]://HOST/USER_DATA_CONNECTOR_MODULE_PATH/server/server.php. So the path part is e.g.: /sites/all/modules/udc/.
  • Constant USER_DATA_CONNECTOR_USE_HTTP_HOST_INSTEAD_OF_LOCALHOST (bool): Forces the client base class to connect to the IP address of the server ($_SERVER['HTTP_HOST']) instead of localhost.

Performance

Some annotations about the performance: As realized during the implementation and testing, the bottleneck according to the performance is not mainly the additional localhost connection, but the full bootstrapping and hash calculation. The server script always bootstraps to the DRUPAL_BOOTSTRAP_VARIABLES state, which does not consume much time. In case of an authentication request, the necessary Drupal core files are included and the user_check_password() function is invoked, which increases the required process time significantly. A second factor reducing the performance is the request of several types of additional user information. There are two information types implemented in the server script: Fields of the core profile module (which is unfortunately deprecated since DP7.2) and common fields (As configured in User>Manage Fields).

The server has a rapid method of retrieving profile module fields from the database with almost no performance losses. For common fields however, the server needs the user_load() function. This causes an in-place full Drupal bootstrap, which is time and memory consuming for a small intermediate script. Here some process times for comparison, where tp = server process time, R=process time ratio with respect to the ping process time, and tr the total request time including the SSL network connection and unserializing. The test ran on an older notebook, the values may vary with the server, code caching etc. You can find the test script "performance.php" in the examples folder.

  • Ping: tr = 73ms, tp = 17ms, R = 1.0
  • List without roles: tr = 81ms, tp = 24ms, R = 1.4
  • List with roles: tr = 83ms, tp = 26ms, R = 1.5
  • Info without fields: tr = 86ms, tp = 26ms, R = 1.5
  • Info with 1 profile fields: tr = 86ms, tp = 29ms, R = 1.7
  • Auth without fields: tr = 186ms, tp = 129ms, R = 7.5
  • Auth with 1 profile field: tr = 191ms, tp = 134ms, R = 7.8
  • Info with 1 common fields: tr = 258ms, tp = 188ms, R = 10.9
  • Auth with 1 common field: tr = 365ms, tp = 295ms, R = 17.0

Conclusion: In order to avoid latencies in your script responses, try not to authenticate the user quite often / every time, instead save the client results (or the whole client object) in your session variables.

api-examples

API Examples

Here some examples in place. You can find the exactly these files in the directory sites/all/modules/udc/examples. To focus on the real matter, all examples include the following file. It is mainly used to include the class files and set the server token:

/**
 * @file
 *
 * Example common include file, used to set the server token, include
 * required classes and define auxillary functions.
 *
 * NOTE: The examples only work on localhost and you must specify the correct
 * token (the same you configured using the module configuration page).
 */

// Set this token to your configured token
$TOKEN = 'change-this-to-the-token-in-your-module-config';

// Check if localhost
if($_SERVER['SERVER_NAME'] != 'localhost') {
  header('Unauthorized', TRUE, 401);
  die("<h1>Not Authorized</h1>");
}

// Include client classes
require_once('../client/DrupalUserAuth.class.inc');
require_once('../client/DrupalUserInfo.class.inc');
require_once('../client/DrupalUserList.class.inc');

// Set token
DrupalUserBase::setToken($TOKEN);
unset($TOKEN);

// Timing functions
static $_timer = 0;
function tic() { global $_timer; $_timer = microtime(true); }
function toc() { global $_timer; return (microtime(true) - $_timer) * 1000.0; @flush(); }

Ping the server to check the connection and token

You can use ping with all DrupalUser***** classes, as the feature is implemented in DrupalUserBase and the other classes are derived from this class.

// Some global settings for all examples are defined here, such as the token.
require_once('enable_examples.inc');

try {
  // Create the user list object
  $dpu = new DrupalUserAuth();

  // Send request 
  $ping = $dpu->ping();
} catch (Exception $e) {
  die("Exception: " . $e->getMessage());
}


Request user authentication

This is an example for a HTTPS basic authentication using the DrupalUserAuth class. If your (external) script is invoked quite often, you should save the response user data in the session instead of sending a local authentication request every time. The reason is not only the additional localhost HTTP connection, but the simple fact that calculating the SHA256 checksum takes Drupal comparatively much processing time.

// Some global settings for all examples are defined here, such as the token.
  require_once('enable_examples.inc');

// Set the roles which shall be accepted here
  $requiredRoles = array('administrator');

  try {

    // Of cause we do not want the browser to send username and password with the
    // request if we are not on an encrypted channel.
    if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on') {
      throw new Exception('Not authorized: No HTTPS');
    }

    // Ask Drupal if the user is valid. If yes, check if the user has the right Role.    
    $userAuthorized = false;
    if (isset($_SERVER['PHP_AUTH_USER']) && trim($_SERVER['PHP_AUTH_USER']) != '') {

      try {
        // Create the user list object
        $dpu = new DrupalUserAuth();

        // Send request (Annotation: You can check login/password as well
        // if the used is blocked in Drupal, to prevent obtaining blocked
        // users, set the 4th argument to true (this is the default as well).
        $dpu->request(
                $_SERVER['PHP_AUTH_USER'], // login name
                $_SERVER['PHP_AUTH_PW'], // password
                null, // email address
                true, // active users only (non blocked)
                true  // include all user fields in the response
        );

        // Check if the login/pass was valid and the user is active
        if ($dpu->valid && $dpu->active) {
          if (empty($roles)) {
            $userAuthorized = true;
          } else {
            foreach ($requiredRoles as $role) {
              if (in_array($role, $dpu->roles)) {
                $userAuthorized = true;
                break;
              }
            }
          }
        }
      } catch (Exception $e) {
        // This catch is only for us to debug, we throw an unauthorized again
        print "Exception: \n\n$e\n\n";
        if (isset($dpu))
          print "----- DEBUG-----\n\n" . $dpu->getDebug() . "\n\n";
        throw new Exception('Not authorized');
      }
    }

    // This is only for us now to force everytime a new login
    if ($userAuthorized) {
      @session_start();
      if (!isset($_SESSION['renew'])) {
        $_SESSION['renew'] = 1;
      } else {
        unset($_SESSION['renew']);
        $userAuthorized = false;
      }
    }

    // Not authorized (yet?) --> Send in the header that a basic auth is required
    // and get the hell out of here.
    if (!$userAuthorized) {
      //header('WWW-Authenticate: Basic realm="Login required"');
      // We always change the relam so that browsers always show the auth.
      // popup again ... this is just for us to test now.
      header('WWW-Authenticate: Basic realm="Login required - ' . time() . '"');
      header('HTTP/1.0 401 Unauthorized');
      throw new Exception('Not authorized');
    }
  } catch (Exception $e) {
    // We abort the script here with the exception message text
    die($e->getMessage());
  }


Request single user information

If you want to get information about a user without checking the password, use the DrupalUserInfo class. The server script response will contain the same information as the DrupalUserAuth class response - except that the password check is skipped and the valid property is true if the user exists.

// Some global settings for all examples are defined here, such as the token.
  require_once('enable_examples.inc');

  try {

    // Check if the script is invoked like userinfo.php?user=<USER NAME>
    if (!isset($_GET['user'])) {
      throw new Exception("USAGE: {$_SERVER['PHP_SELF']}?user=username");
    }

    // Create the user list object    
    $dpu = new DrupalUserInfo();

    // Send the request
    $dpu->request(
            trim($_GET['user']), // login name
            null, // email address
            true, // active users only (non blocked)
            true  // include all user fields in the response
    );
  } catch (Exception $e) {
    die($e->getMessage());
  }


Request a user list

There is a variety of situations where it is necessary to get a simple list of users from the Drupal database. The method of getting these data using User Data Connector is the DrupalUserList class:

// Some global settings for all examples are defined here, such as the token.
  require_once('enable_examples.inc');

  try {
    // Create the user list object
    $dpu = new DrupalUserList();

    // Send the request
    $dpu->request(
            false, // List all users, not only active users
            true   // List with user roles
    );
  } catch (Exception $e) {
    die($e->getMessage());
  }

// Display resulte
  header('Content-Type: text/html; charset=utf-8');


Troubleshooting

  • If your client API throws an exception "The path to the server script does not contain the server document root", then the automatic detection of the server script url failed. This can happen if you for example use symlinks or hard links in the Drupal directory structure. You can get around this problem by defining the URI path to the module yourself:
    define("USER_DATA_CONNECTOR_MODULE_PATH", "/[some directory]/sites/all/modules/udc/");
    

    The API will then try to access the server script using the URI: "http[s]://[your.server]/[some directory]/sites/all/modules/udc/server/server.php".

    You can omit leading or trailing slashes, and define the constant before or after including the API files. It is used during the request() call.

    The feature is available in udc 7.x-1.0rc2 and later.

  • If your client cannot access the server script, it is possible that your http server is not generally listening at port 80/443, but only to a specific IP address. For e.g. Apache2, this would be specified using the setting in httpd.conf (or in sites/available/[...]) Listen 192.168.0.111:80 instead of Listen 80. This means that you would need to tell the API not to connect to localhost but to the server IP address. Use the setting
    define("USER_DATA_CONNECTOR_USE_HTTP_HOST_INSTEAD_OF_LOCALHOST", TRUE);
    

    for this. If you ticked the checkbox "SSL only" in the config menu, ensure you defined

    define("USER_DATA_CONNECTOR_USE_SSL", TRUE);
    

    before you invoke the DrupalUserBase::request() method.



UDC use case: SabreDAV integration

You can use UDC to implement the Sabre_DAV_Auth_IBackend interface, as shown in this source code:

<?php
require_once 'Sabre/autoload.php';

/**
 * DP connected user authentification
 */
class Sabre_DAV_Auth_Backend_Basic_DrupalConnected implements Sabre_DAV_Auth_IBackend {

    /**
     * @var DrupalUserAuth
     */
    public $dpu = null;

    /**
     * Returns information about the currently logged in username.
     * If nobody is currently logged in, this method should return null.
     * @return string|null
     */
    public function getCurrentUser() {
        return !$this->dpu ? null : $this->dpu->name;
    }

    /**
     * Authenticates the user based on the current request.
     * If authentication is succesful, true must be returned.
     * If authentication fails, an exception must be thrown.
     * @throws Sabre_DAV_Exception_NotAuthenticated
     * @return bool
     */
    public function authenticate(Sabre_DAV_Server $server,$realm) {

        // Assuming we only accept https for basic auth
        if(!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on') {
            throw new Sabre_DAV_Exception_NotAuthenticated('HTTPS only');
        }

        // Prepare SabreDav auth instances, get user & password pair
        $auth = new Sabre_HTTP_BasicAuth();
        $auth->setHTTPRequest($server->httpRequest);
        $auth->setHTTPResponse($server->httpResponse);
        $auth->setRealm($realm);
        $userpass = $auth->getUserPass();

        // Pre check
        if(!$userpass) {
            $auth->requireLogin();
            throw new Sabre_DAV_Exception_NotAuthenticated('No basic authentication headers were found');
        }

        // Assuming DRUPAL ROOT = $_SERVER['DOCUMENT_ROOT']
        require_once($_SERVER['DOCUMENT_ROOT'] . '/sites/all/modules/udc/client/DrupalUserAuth.class.inc');

        // The token used to connect to DP
        DrupalUserBase::setToken("---my---token---here---");

        // Authentification request
        $dpu = new DrupalUserAuth();
        $dpu->request(
            $userpass[0],    // user name
            $userpass[1],     // pass
            null,            // email
            true,            // active users only
            null            // no profile fields to fetch
        );

        // Validation
        if (!$dpu->valid) {
            $auth->requireLogin();
            throw new Sabre_DAV_Exception_NotAuthenticated('Username or password does not match');
        } else {
            $this->dpu = $dpu;
            return true;
        }
    }
}

// Directory structure of this sample code:
//
// drwx  - document root (www-data)
// dr-x     - webdav
// -r--        - index.php
// drwx        - davfiles
//                 (DAV FILES ARE IN HERE)
// drw-        - data
// -rw-           - locks
// dr-x        - Sabre
//
$rootDirectory = new Sabre_DAV_FS_Directory('davfiles');
$server = new Sabre_DAV_Server($rootDirectory);
$server->setBaseUri('/webdav/');
// The lock manager is reponsible for making sure users don't overwrite each others changes.
// Change 'data' to a different directory, if you're storing your data somewhere else.
$lockBackend = new Sabre_DAV_Locks_Backend_File('data/locks');
$lockPlugin = new Sabre_DAV_Locks_Plugin($lockBackend);
$server->addPlugin($lockPlugin);
$authBackend = new Sabre_DAV_Auth_Backend_Basic_DrupalConnected();
$authPlugin = new Sabre_DAV_Auth_Plugin($authBackend,'WebDAV area');
$server->addPlugin($authPlugin);
$server->exec(); // All we need to do now, is to fire up the server
?>