Dummy OpenID Provider used with SimpleTest.

The provider simply responds positively to all authentication requests. In addition to a Provider Endpoint (a URL used for Drupal to communicate with the provider using the OpenID Authentication protocol) the module provides URLs used by the various discovery mechanisms.

When a user enters an OpenID identity, the Relying Party (in the testing scenario, this is the OpenID module) looks up the URL of the Provider Endpoint using one of several discovery mechanisms. The Relying Party then redirects the user to Provider Endpoint. The provider verifies the user's identity and redirects the user back to the Relying Party accompanied by a signed message confirming the identity. Before redirecting to a provider for the first time, the Relying Party fetches a secret MAC key from the provider by doing a direct "associate" HTTP request to the Provider Endpoint. This key is used for verifying the signed messages from the provider.

File

modules/openid/tests/openid_test.module
View source
<?php

/**
 * @file
 * Dummy OpenID Provider used with SimpleTest.
 *
 * The provider simply responds positively to all authentication requests. In
 * addition to a Provider Endpoint (a URL used for Drupal to communicate with
 * the provider using the OpenID Authentication protocol) the module provides
 * URLs used by the various discovery mechanisms.
 *
 * When a user enters an OpenID identity, the Relying Party (in the testing
 * scenario, this is the OpenID module) looks up the URL of the Provider
 * Endpoint using one of several discovery mechanisms. The Relying Party then
 * redirects the user to Provider Endpoint. The provider verifies the user's
 * identity and redirects the user back to the Relying Party accompanied by a
 * signed message confirming the identity. Before redirecting to a provider for
 * the first time, the Relying Party fetches a secret MAC key from the provider
 * by doing a direct "associate" HTTP request to the Provider Endpoint. This
 * key is used for verifying the signed messages from the provider.
 */

/**
 * Implements hook_menu().
 */
function openid_test_menu() {
  $items['openid-test/yadis/xrds'] = array(
    'title' => 'XRDS service document',
    'page callback' => 'openid_test_yadis_xrds',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/yadis/x-xrds-location'] = array(
    'title' => 'Yadis discovery using X-XRDS-Location header',
    'page callback' => 'openid_test_yadis_x_xrds_location',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/yadis/http-equiv'] = array(
    'title' => 'Yadis discovery using <meta http-equiv="X-XRDS-Location" ...>',
    'page callback' => 'openid_test_yadis_http_equiv',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/html/openid1'] = array(
    'title' => 'HTML-based discovery using <link rel="openid.server" ...>',
    'page callback' => 'openid_test_html_openid1',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/html/openid2'] = array(
    'title' => 'HTML-based discovery using <link rel="openid2.provider" ...>',
    'page callback' => 'openid_test_html_openid2',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/endpoint'] = array(
    'title' => 'OpenID Provider Endpoint',
    'page callback' => 'openid_test_endpoint',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/redirect'] = array(
    'title' => 'OpenID Provider Redirection Point',
    'page callback' => 'openid_test_redirect',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['openid-test/redirected/%/%'] = array(
    'title' => 'OpenID Provider Final URL',
    'page callback' => 'openid_test_redirected_method',
    'page arguments' => array(
      2,
      3,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_menu_site_status_alter().
 */
function openid_test_menu_site_status_alter(&$menu_site_status, $path) {

  // Allow access to openid endpoint and identity even in offline mode.
  if ($menu_site_status == MENU_SITE_OFFLINE && user_is_anonymous() && in_array($path, array(
    'openid-test/yadis/xrds',
    'openid-test/endpoint',
  ))) {
    $menu_site_status = MENU_SITE_ONLINE;
  }
}

/**
 * Menu callback; XRDS document that references the OP Endpoint URL.
 */
function openid_test_yadis_xrds() {
  if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') {

    // Only respond to XRI requests for one specific XRI. The is used to verify
    // that the XRI has been properly encoded. The "+" sign in the _xrd_r query
    // parameter is decoded to a space by PHP.
    if (arg(3) == 'xri') {
      if (variable_get('clean_url', 0)) {
        if (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml') {
          drupal_not_found();
        }
      }
      else {

        // Drupal cannot properly emulate an XRI proxy resolver using unclean
        // URLs, so the arguments gets messed up.
        if (arg(4) . '/' . arg(5) != '@example*résumé;%25?_xrd_r=application/xrds xml') {
          drupal_not_found();
        }
      }
    }
    drupal_add_http_header('Content-Type', 'application/xrds+xml');
    print '<?xml version="1.0" encoding="UTF-8"?>';
    if (!empty($_GET['doctype'])) {
      print "\n<!DOCTYPE dct [ <!ELEMENT blue (#PCDATA)> ]>\n";
    }
    print '
      <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)" xmlns:openid="http://openid.net/xmlns/1.0">
        <XRD>
          <Status cid="' . check_plain(variable_get('openid_test_canonical_id_status', 'verified')) . '"/>
          <ProviderID>xri://@</ProviderID>
          <CanonicalID>http://example.com/user</CanonicalID>
          <Service>
            <Type>http://example.com/this-is-ignored</Type>
          </Service>
          <Service priority="5">
            <Type>http://openid.net/signon/1.0</Type>
            <URI>http://example.com/this-is-only-openid-1.0</URI>
          </Service>
          <Service priority="10">
            <Type>http://specs.openid.net/auth/2.0/signon</Type>
            <Type>http://openid.net/srv/ax/1.0</Type>
            <URI>' . url('openid-test/endpoint', array(
      'absolute' => TRUE,
    )) . '</URI>
            <LocalID>http://example.com/xrds</LocalID>
          </Service>
          <Service priority="15">
            <Type>http://specs.openid.net/auth/2.0/signon</Type>
            <URI>http://example.com/this-has-too-low-priority</URI>
          </Service>
          <Service>
            <Type>http://specs.openid.net/auth/2.0/signon</Type>
            <URI>http://example.com/this-has-too-low-priority</URI>
          </Service>
          ';
    if (arg(3) == 'server') {
      print '
          <Service>
            <Type>http://specs.openid.net/auth/2.0/server</Type>
            <URI>http://example.com/this-has-too-low-priority</URI>
          </Service>
          <Service priority="20">
            <Type>http://specs.openid.net/auth/2.0/server</Type>
            <URI>' . url('openid-test/endpoint', array(
        'absolute' => TRUE,
      )) . '</URI>
            <LocalID>' . url('openid-test/yadis/xrds/server', array(
        'absolute' => TRUE,
      )) . '</LocalID>
          </Service>';
    }
    elseif (arg(3) == 'delegate') {
      print '
          <Service priority="0">
            <Type>http://specs.openid.net/auth/2.0/signon</Type>
            <Type>http://openid.net/srv/ax/1.0</Type>
            <URI>' . url('openid-test/endpoint', array(
        'absolute' => TRUE,
      )) . '</URI>
            <openid:Delegate>http://example.com/xrds-delegate</openid:Delegate>
          </Service>';
    }
    print '
        </XRD>
      </xrds:XRDS>';
  }
  else {
    return t('This is a regular HTML page. If the client sends an Accept: application/xrds+xml header when requesting this URL, an XRDS document is returned.');
  }
}

/**
 * Menu callback; regular HTML page with an X-XRDS-Location HTTP header.
 */
function openid_test_yadis_x_xrds_location() {
  drupal_add_http_header('X-XRDS-Location', url('openid-test/yadis/xrds', array(
    'absolute' => TRUE,
  )));
  return t('This page includes an X-RDS-Location HTTP header containing the URL of an XRDS document.');
}

/**
 * Menu callback; regular HTML page with <meta> element.
 */
function openid_test_yadis_http_equiv() {
  $element = array(
    '#tag' => 'meta',
    '#attributes' => array(
      'http-equiv' => 'X-XRDS-Location',
      'content' => url('openid-test/yadis/xrds', array(
        'absolute' => TRUE,
      )),
    ),
  );
  drupal_add_html_head($element, 'openid_test_yadis_http_equiv');
  return t('This page includes a &lt;meta equiv=...&gt; element containing the URL of an XRDS document.');
}

/**
 * Menu callback; regular HTML page with OpenID 1.0 <link> element.
 */
function openid_test_html_openid1() {
  drupal_add_html_head_link(array(
    'rel' => 'openid.server',
    'href' => url('openid-test/endpoint', array(
      'absolute' => TRUE,
    )),
  ));
  drupal_add_html_head_link(array(
    'rel' => 'openid.delegate',
    'href' => 'http://example.com/html-openid1',
  ));
  return t('This page includes a &lt;link rel=...&gt; element containing the URL of an OpenID Provider Endpoint.');
}

/**
 * Menu callback; regular HTML page with OpenID 2.0 <link> element.
 */
function openid_test_html_openid2() {
  drupal_add_html_head_link(array(
    'rel' => 'openid2.provider',
    'href' => url('openid-test/endpoint', array(
      'absolute' => TRUE,
    )),
  ));
  drupal_add_html_head_link(array(
    'rel' => 'openid2.local_id',
    'href' => 'http://example.com/html-openid2',
  ));
  return t('This page includes a &lt;link rel=...&gt; element containing the URL of an OpenID Provider Endpoint.');
}

/**
 * Menu callback; OpenID Provider Endpoint.
 *
 * It accepts "associate" requests directly from the Relying Party, and
 * "checkid_setup" requests made by the user's browser based on HTTP redirects
 * (in OpenID 1) or HTML forms (in OpenID 2) generated by the Relying Party.
 */
function openid_test_endpoint() {
  switch ($_REQUEST['openid_mode']) {
    case 'associate':
      _openid_test_endpoint_associate();
      break;
    case 'checkid_setup':
      _openid_test_endpoint_authenticate();
      break;
  }
}

/**
 * Menu callback; redirect during Normalization/Discovery.
 */
function openid_test_redirect($count = 0) {
  if ($count == 0) {
    $url = variable_get('openid_test_redirect_url', '');
  }
  else {
    $url = url('openid-test/redirect/' . --$count, array(
      'absolute' => TRUE,
    ));
  }
  $http_response_code = variable_get('openid_test_redirect_http_reponse_code', 301);
  header('Location: ' . $url, TRUE, $http_response_code);
  exit;
}

/**
 * Menu callback; respond with appropriate callback.
 */
function openid_test_redirected_method($method1, $method2) {
  return call_user_func('openid_test_' . $method1 . '_' . $method2);
}

/**
 * OpenID endpoint; handle "associate" requests (see OpenID Authentication 2.0,
 * section 8).
 *
 * The purpose of association is to send the secret MAC key to the Relying Party
 * using Diffie-Hellman key exchange. The MAC key is used in subsequent
 * "authenticate" requests. The "associate" request is made by the Relying Party
 * (in the testing scenario, this is the OpenID module that communicates with
 * the endpoint using drupal_http_request()).
 */
function _openid_test_endpoint_associate() {
  module_load_include('inc', 'openid');

  // Use default parameters for Diffie-Helmann key exchange.
  $mod = OPENID_DH_DEFAULT_MOD;
  $gen = OPENID_DH_DEFAULT_GEN;

  // Generate private Diffie-Helmann key.
  $r = _openid_dh_rand($mod);
  $private = _openid_math_add($r, 1);

  // Calculate public Diffie-Helmann key.
  $public = _openid_math_powmod($gen, $private, $mod);

  // Calculate shared secret based on Relying Party's public key.
  $cpub = _openid_dh_base64_to_long($_REQUEST['openid_dh_consumer_public']);
  $shared = _openid_math_powmod($cpub, $private, $mod);

  // Encrypt the MAC key using the shared secret.
  $enc_mac_key = base64_encode(_openid_dh_xorsecret($shared, base64_decode(variable_get('mac_key'))));

  // Generate response including our public key and the MAC key. Using our
  // public key and its own private key, the Relying Party can calculate the
  // shared secret, and with this it can decrypt the encrypted MAC key.
  $response = array(
    'ns' => 'http://specs.openid.net/auth/2.0',
    'assoc_handle' => 'openid-test',
    'session_type' => $_REQUEST['openid_session_type'],
    'assoc_type' => $_REQUEST['openid_assoc_type'],
    'expires_in' => '3600',
    'dh_server_public' => _openid_dh_long_to_base64($public),
    'enc_mac_key' => $enc_mac_key,
  );

  // Respond to Relying Party in the special Key-Value Form Encoding (see OpenID
  // Authentication 1.0, section 4.1.1).
  drupal_add_http_header('Content-Type', 'text/plain');
  print _openid_create_message($response);
}

/**
 * OpenID endpoint; handle "authenticate" requests.
 *
 * All requests result in a successful response. The request is a GET or POST
 * made by the user's browser based on an HTML form or HTTP redirect generated
 * by the Relying Party. The user is redirected back to the Relying Party using
 * a URL containing a signed message in the query string confirming the user's
 * identity.
 */
function _openid_test_endpoint_authenticate() {
  module_load_include('inc', 'openid');
  $expected_identity = variable_get('openid_test_identity');
  if ($expected_identity && $_REQUEST['openid_identity'] != $expected_identity) {
    $response = variable_get('openid_test_response', array()) + array(
      'openid.ns' => OPENID_NS_2_0,
      'openid.mode' => 'error',
      'openid.error' => 'Unexpted identity',
    );
    drupal_add_http_header('Content-Type', 'text/plain');
    header('Location: ' . url($_REQUEST['openid_return_to'], array(
      'query' => $response,
      'external' => TRUE,
    )));
    return;
  }

  // Generate unique identifier for this authentication.
  $nonce = _openid_nonce();

  // Generate response containing the user's identity.
  $response = variable_get('openid_test_response', array()) + array(
    'openid.ns' => OPENID_NS_2_0,
    'openid.mode' => 'id_res',
    'openid.op_endpoint' => url('openid-test/endpoint', array(
      'absolute' => TRUE,
    )),
    'openid.claimed_id' => !empty($_REQUEST['openid_claimed_id']) ? $_REQUEST['openid_claimed_id'] : '',
    'openid.identity' => $_REQUEST['openid_identity'],
    'openid.return_to' => $_REQUEST['openid_return_to'],
    'openid.response_nonce' => $nonce,
    'openid.assoc_handle' => 'openid-test',
  );
  if (isset($response['openid.signed'])) {
    $keys_to_sign = explode(',', $response['openid.signed']);
  }
  else {

    // Unless openid.signed is explicitly defined, all keys are signed.
    $keys_to_sign = array();
    foreach ($response as $key => $value) {

      // Strip off the "openid." prefix.
      $keys_to_sign[] = substr($key, 7);
    }
    $response['openid.signed'] = implode(',', $keys_to_sign);
  }

  // Sign the message using the MAC key that was exchanged during association.
  $association = new stdClass();
  $association->mac_key = variable_get('mac_key');
  if (!isset($response['openid.sig'])) {
    $response['openid.sig'] = _openid_signature($association, $response, $keys_to_sign);
  }

  // Put the signed message into the query string of a URL supplied by the
  // Relying Party, and redirect the user.
  drupal_add_http_header('Content-Type', 'text/plain');
  header('Location: ' . url($_REQUEST['openid_return_to'], array(
    'query' => $response,
    'external' => TRUE,
  )));
}

Functions

Namesort descending Description
openid_test_endpoint Menu callback; OpenID Provider Endpoint.
openid_test_html_openid1 Menu callback; regular HTML page with OpenID 1.0 <link> element.
openid_test_html_openid2 Menu callback; regular HTML page with OpenID 2.0 <link> element.
openid_test_menu Implements hook_menu().
openid_test_menu_site_status_alter Implements hook_menu_site_status_alter().
openid_test_redirect Menu callback; redirect during Normalization/Discovery.
openid_test_redirected_method Menu callback; respond with appropriate callback.
openid_test_yadis_http_equiv Menu callback; regular HTML page with <meta> element.
openid_test_yadis_xrds Menu callback; XRDS document that references the OP Endpoint URL.
openid_test_yadis_x_xrds_location Menu callback; regular HTML page with an X-XRDS-Location HTTP header.
_openid_test_endpoint_associate OpenID endpoint; handle "associate" requests (see OpenID Authentication 2.0, section 8).
_openid_test_endpoint_authenticate OpenID endpoint; handle "authenticate" requests.