You are here

janrain_capture.api.inc in Janrain Registration 6

Same filename and directory in other branches
  1. 7 janrain_capture.api.inc

API Client for making calls to the Janrain Capture web service

File

janrain_capture.api.inc
View source
<?php

/**
 * @file
 * API Client for making calls to the Janrain Capture web service
 */
class JanrainCaptureApi {
  protected $args;
  protected $captureAddr;

  /**
   * Retrives API access credentials from settings.
   */
  function __construct() {
    $janrain_capture_main = variable_get('janrain_capture_main', array());
    $janrain_capture_optional = variable_get('janrain_capture_optional', array());
    $this->args = array();
    $this->args['client_id'] = isset($janrain_capture_main['capture_client_id']) ? $janrain_capture_main['capture_client_id'] : '';
    $this->args['client_secret'] = isset($janrain_capture_main['capture_client_secret']) ? $janrain_capture_main['capture_client_secret'] : '';
    $this->captureAddr = !empty($janrain_capture_main['capture_address']) ? $janrain_capture_main['capture_address'] : '';
  }

  /**
   * Performs the HTTP request.
   *
   * @param string $command
   *   The Capture command to perform
   * @param array $arg_array
   *   The data set to pass via POST
   * @param string $access_token
   *   The client access token to use when performing user-specific calls
   *
   * @return mixed
   *   The HTTP request result data
   */
  protected function call($command, $arg_array = NULL, $access_token = NULL) {
    $url = "https://" . $this->captureAddr . "/{$command}";
    $headers = array(
      'Content-Type' => 'application/x-www-form-urlencoded',
      'User-Agent' => 'Drupal',
    );
    if (isset($access_token)) {
      $headers['Authorization'] = "OAuth {$access_token}";
    }
    if (isset($arg_array)) {
      $arg_array = array_merge($arg_array, $this->args);
      $result = drupal_http_request($url, $headers, "POST", http_build_query($arg_array, '', '&'));
    }
    else {
      $result = drupal_http_request($url, $headers);
    }
    if (!isset($result->data) || $result->code != '200') {
      $this
        ->reportError($result);
      return FALSE;
    }
    $json_data = json_decode($result->data, TRUE);

    // NULL decoded value indicates a parse error.
    if (!isset($json_data)) {
      $json_data['stat'] = 'error';
      $json_data['code'] = '0';
      $json_data['error'] = 'JSON parse error for: ' . $result->data;
    }
    if ($json_data['stat'] == 'error') {
      $error = new stdClass();
      $error->code = $json_data['code'];
      $error->error = $json_data['error'];
      $this
        ->reportError($error);
      return FALSE;
    }
    return $json_data;
  }

  /**
   * Updates session variables with Capture user tokens
   *
   * @param string $json_data
   *   The data received from the HTTP request containing the tokens
   */
  protected function updateCaptureSession($json_data) {
    $_SESSION['janrain_capture_access_token'] = $json_data['access_token'];
    $_SESSION['janrain_capture_refresh_token'] = $this
      ->refreshTokenProtect($json_data['refresh_token']);
    $_SESSION['janrain_capture_expires_in'] = REQUEST_TIME + $json_data['expires_in'];
    $password_recover = isset($json_data['transaction_state']['capture']['password_recover']) && $json_data['transaction_state']['capture']['password_recover'] == TRUE ? TRUE : FALSE;
    $_SESSION['janrain_capture_password_recover'] = $password_recover;
    if (isset($json_data['transaction_state']['capture']['action'])) {
      $_SESSION['janrain_capture_action'] = $json_data['transaction_state']['capture']['action'];
    }
  }

  /**
   * Helper function to conceal the refresh token and set a cookie.
   *
   * @param string $raw_refresh_token
   *   The plain-text refresh token received from the capture server.
   *
   * @return string
   *   A base64 encoded, encrypted token.
   */
  protected function refreshTokenProtect($raw_refresh_token) {
    $len = strlen($raw_refresh_token);

    // Use a random pad of matching length to protect the token value.
    $pad = $this
      ->random_bytes($len);
    $cookie_pad = base64_encode($pad);

    // XOR the refresh token and encode the binary value.
    $protected_refresh_token = base64_encode($raw_refresh_token ^ $pad);

    // Use the same liftime and other params as for a Drupal session cookie.
    $params = session_get_cookie_params();
    $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
    setcookie('janrain_capture_pad', $cookie_pad, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
    return $protected_refresh_token;
  }

  /**
   * Helper function to decode a refresh token using a pad from a cookie.
   *
   * @param string $protcted_refresh_token
   *   The base64 encoded, encrypted token.
   *
   * @return mixed
   *   A plain text token string, or NULL on failure.
   */
  protected function refreshTokenReveal($protected_refresh_token) {
    $raw_refresh_token = NULL;
    $bin = base64_decode($protected_refresh_token);
    if ($bin && isset($_COOKIE['janrain_capture_pad'])) {
      $pad = base64_decode($_COOKIE['janrain_capture_pad']);
      if ($pad && strlen($bin) == strlen($pad)) {
        $raw_refresh_token = $bin ^ $pad;
      }
    }
    return $raw_refresh_token;
  }

  /**
   * Helper function for the Engage web API wrappers.
   *
   * @param stdClass $result
   *   Result containing error code and message
   */
  protected function reportError($result) {
    watchdog('janrain_capture', 'Capture web API seems to be inaccessible due to "%error".', array(
      '%error' => $result->code . ' ' . $result->error,
    ), WATCHDOG_WARNING);
    drupal_set_message(t('Capture web API seems to be inaccessible because of error "%error".', array(
      '%error' => $result->code . ' ' . $result->error,
    )), 'error');
  }

  /**
   * Perform the exchange to generate a new Access Token
   *
   * @param string $auth_code
   *   The authorization token to use for the exchange
   * @param string $redirect_uri
   *   The redirect_uri string to match for the exchange
   */
  public function newAccessToken($auth_code, $redirect_uri) {
    $command = "oauth/token";
    $arg_array = array(
      'code' => $auth_code,
      'redirect_uri' => $redirect_uri,
      'grant_type' => 'authorization_code',
    );
    $json_data = $this
      ->call($command, $arg_array);
    if ($json_data) {
      $this
        ->updateCaptureSession($json_data);
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Retrieves a new access_token/refresh_token set
   *
   * @return boolean
   *   Boolean success or failure
   */
  function refreshAccessToken() {
    if (empty($_SESSION['janrain_capture_refresh_token'])) {
      return FALSE;
    }
    $refresh_token = $this
      ->refreshTokenReveal($_SESSION['janrain_capture_refresh_token']);
    $command = "oauth/token";
    $arg_array = array(
      'refresh_token' => $refresh_token,
      'grant_type' => 'refresh_token',
    );
    $json_data = $this
      ->call($command, $arg_array);
    if ($json_data) {
      $this
        ->updateCaptureSession($json_data);
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Retrives the user entity from Capture
   *
   * @param boolean $can_refresh
   *   Allow this function to refresh the token set if needed
   *
   * @return mixed
   *   The entity retrieved or null
   */
  public function loadUserEntity($can_refresh = TRUE) {
    if (empty($_SESSION['janrain_capture_access_token'])) {
      return NULL;
    }
    $user_entity = NULL;
    $need_to_refresh = FALSE;

    // Check if we need to refresh the access token.
    if (REQUEST_TIME >= $_SESSION['janrain_capture_expires_in']) {
      $need_to_refresh = TRUE;
    }
    else {
      $user_entity = $this
        ->call('entity', array(), $_SESSION['janrain_capture_access_token']);
      if (isset($user_entity['code']) && $user_entity['code'] == '414') {
        $need_to_refresh = TRUE;
      }
    }

    // If necessary, refresh the access token and try to fetch the entity again.
    if ($need_to_refresh && $can_refresh) {
      if ($this
        ->refreshAccessToken()) {
        $user_entity = $this
          ->loadUserEntity(FALSE);
      }
    }

    // Return NULL if there is an error code.
    return isset($user_entity['code']) ? NULL : $user_entity;
  }

  /**
   * Returns a string of highly randomized bytes (over the full 8-bit range).
   *
   * This function is better than simply calling mt_rand() or any other built-in
   * PHP function because it can return a long string of bytes (compared to < 4
   * bytes normally from mt_rand()) and uses the best available pseudo-random
   * source.
   *
   * @param $count
   *   The number of characters (bytes) to return in the string.
   *
   * @return string
   */
  protected function random_bytes($count) {

    // $random_state does not use drupal_static as it stores random bytes.
    static $random_state, $bytes, $php_compatible;

    // Initialize on the first call. The contents of $_SERVER includes a mix of
    // user-specific and system information that varies a little with each page.
    if (!isset($random_state)) {
      $random_state = print_r($_SERVER, TRUE);
      if (function_exists('getmypid')) {

        // Further initialize with the somewhat random PHP process ID.
        $random_state .= getmypid();
      }
      $bytes = '';
    }
    if (strlen($bytes) < $count) {

      // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
      // locking on Windows and rendered it unusable.
      if (!isset($php_compatible)) {
        $php_compatible = version_compare(PHP_VERSION, '5.3.4', '>=');
      }

      // /dev/urandom is available on many *nix systems and is considered the
      // best commonly available pseudo-random source.
      if ($fh = @fopen('/dev/urandom', 'rb')) {

        // PHP only performs buffered reads, so in reality it will always read
        // at least 4096 bytes. Thus, it costs nothing extra to read and store
        // that much so as to speed any additional invocations.
        $bytes .= fread($fh, max(4096, $count));
        fclose($fh);
      }
      elseif ($php_compatible && function_exists('openssl_random_pseudo_bytes')) {
        $bytes .= openssl_random_pseudo_bytes($count - strlen($bytes));
      }

      // If /dev/urandom is not available or returns no bytes, this loop will
      // generate a good set of pseudo-random bytes on any system.
      // Note that it may be important that our $random_state is passed
      // through hash() prior to being rolled into $output, that the two hash()
      // invocations are different, and that the extra input into the first one -
      // the microtime() - is prepended rather than appended. This is to avoid
      // directly leaking $random_state via the $output stream, which could
      // allow for trivial prediction of further "random" numbers.
      while (strlen($bytes) < $count) {
        $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
        $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
      }
    }
    $output = substr($bytes, 0, $count);
    $bytes = substr($bytes, $count);
    return $output;
  }

}

Classes

Namesort descending Description
JanrainCaptureApi @file API Client for making calls to the Janrain Capture web service