整合Yubikey

时间:2011-08-19 20:24:03

标签: php codeigniter authentication

我打算为管理员添加多因素身份验证到我正在处理的网站。我决定使用Yubikey因为它的简单性,但我遇到了如何实现它的障碍。

有没有人有一个如何保存Yubikey的好例子,以及如何验证输入的Yubikey(登录时)是保存的Yubikey,并且是有效的OTP?我在Yubikey网站上看到了一个完整的网站示例,但我似乎无法找到我正在寻找的位。我也不想使用PEAR

由于

2 个答案:

答案 0 :(得分:1)

正如Wesley指出的那样,似乎没有很多关于这个产品的知识,所以我花时间修改了google code上出来的精彩图书馆,以便与codeigniter合作。

遗憾的是,在执行此操作时,我不得不删除PEAR错误消息,因此错误现在只返回通用FALSE。课程如下,我希望有人认为它很有用

libaries/yubikey.php

<?php
  /**
   * Class for verifying Yubico One-Time-Passcodes
   *
   * @category    Auth
   * @package     Auth_Yubico
   * @author      Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>, modified by mazzzzz to work with CodeIgniter
   * @copyright   2007, 2008, 2009, 2010 Yubico AB
   * @license     http://opensource.org/licenses/bsd-license.php New BSD License
   * @version     2.0
   * @link        http://www.yubico.com/
   */

/**
 * Class for verifying Yubico One-Time-Passcodes
 *
 * Simple example:
 * <code>
 * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
 * $SavedPrefix = "ccbbddeertkrct"; //Get using the below to lines that set $pkey.  This will verify the yubikey is the one you want aswell as a valid OTP
 *
 * # Generate a new id+key from https://api.yubico.com/get-api-key/ and put them into the yubikey config file
 * $yubi = $this->load->library('yubikey', array('https'=>1, 'verifyhttps'=>1));
 * $auth = $yubi->verify($otp);
 *
 * $pkey = $this->yubikey->parsePasswordOTP($this->input->post('key'));
 * $pkey = $pkey["prefix"];
 *
 *
 * if ($auth === FALSE || $pkey != $SavedPrefix) {
 *    print "<p>Not authenticated</p>";
 * } else {
 *    print "<p>You are authenticated!</p>";
 * }
 * </code>
 */
class yubikey
{
    /**#@+
     * @access private
     */

    /**
     * Yubico client ID
     * @var string
     */
    var $_id;

    /**
     * Yubico client key
     * @var string
     */
    var $_key;

    /**
     * URL part of validation server
     * @var string
     */
    var $_url;

    /**
     * List with URL part of validation servers
     * @var array
     */
    var $_url_list;

    /**
     * index to _url_list
     * @var int
     */
    var $_url_index;

    /**
     * Last query to server
     * @var string
     */
    var $_lastquery;

    /**
     * Response from server
     * @var string
     */
    var $_response;

    /**
     * Flag whether to use https or not.
     * @var boolean
     */
    var $_https;

    /**
     * Flag whether to verify HTTPS server certificates or not.
     * @var boolean
     */
    var $_httpsverify;

    /**
     * CodeIgniter instance pointer
     * @var object
     */
    var $_CI;

    /**
     * Yubikey config file
     * @var array
     */
    var $_config;
    /**
     * Constructor
     *
     * Sets up the object
     * @param    boolean $https  Flag whether to use https (optional)
     * @param    boolean $httpsverify  Flag whether to use verify HTTPS
     *                                 server certificates (optional,
     *                                 default true)
     * @access public
     */
    function __construct($configs)
    {
        $optionalDefaults = array('https' => 1, 'httpsverify' => 1);

        //set defaults
        foreach ($optionalDefaults as $n=>$v)
        {
            if (!in_array($n, $configs))
                $configs[$n] = $v;
        }

        //Load config files etc.
        $this->_CI = get_instance();
        $this->_CI->config->load('yubikey', TRUE, TRUE);
        $this->_config = $this->_CI->config->item('yubikey');

        //Set values for class

        $this->_id =  $this->_config["cid"];
        $this->_key = base64_decode($this->_config["key"]);
        $this->_https = $configs["https"];
        $this->_httpsverify = $configs["httpsverify"];
    }

    /**
     * Specify to use a different URL part for verification.
     * The default is "api.yubico.com/wsapi/verify".
     *
     * @param  string $url  New server URL part to use
     * @access public
     */
    function setURLpart($url)
    {
        $this->_url = $url;
    }

    /**
     * Get URL part to use for validation.
     *
     * @return string  Server URL part
     * @access public
     */
    function getURLpart()
    {
        return $this->_config["apiurl"];
    }


    /**
     * Get next URL part from list to use for validation.
     *
     * @return mixed string with URL part of false if no more URLs in list
     * @access public
     */
    function getNextURLpart()
    {
      if ($this->_url_list) $url_list=$this->_url_list;
      else $url_list=array('api.yubico.com/wsapi/2.0/verify',
                   'api2.yubico.com/wsapi/2.0/verify', 
                   'api3.yubico.com/wsapi/2.0/verify', 
                   'api4.yubico.com/wsapi/2.0/verify',
                   'api5.yubico.com/wsapi/2.0/verify');

      if ($this->_url_index>=count($url_list)) return false;
      else return $url_list[$this->_url_index++];
    }

    /**
     * Resets index to URL list
     *
     * @access public
     */
    function URLreset()
    {
      $this->_url_index=0;
    }

    /**
     * Add another URLpart.
     *
     * @access public
     */
    function addURLpart($URLpart) 
    {
      $this->_url_list[]=$URLpart;
    }

    /**
     * Return the last query sent to the server, if any.
     *
     * @return string  Request to server
     * @access public
     */
    function getLastQuery()
    {
        return $this->_lastquery;
    }

    /**
     * Return the last data received from the server, if any.
     *
     * @return string  Output from server
     * @access public
     */
    function getLastResponse()
    {
        return $this->_response;
    }

    /**
     * Parse input string into password, yubikey prefix,
     * ciphertext, and OTP.
     *
     * @param  string    Input string to parse
     * @param  string    Optional delimiter re-class, default is '[:]'
     * @return array     Keyed array with fields
     * @access public
     */
    function parsePasswordOTP($str, $delim = '[:]')
    {
        if (!preg_match("/^((.*)" . $delim . ")?" .
                "(([cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{0,16})" .
                "([cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{32}))$/",
                $str, $matches)) {
            return false;
        }
        $ret['password'] = $matches[2];
        $ret['otp'] = $matches[3];
        $ret['prefix'] = $matches[4];
        $ret['ciphertext'] = $matches[5];
        return $ret;
    }

    /* TODO? Add functions to get parsed parts of server response? */

    /**
     * Parse parameters from last response
     *
     * example: getParameters("timestamp", "sessioncounter", "sessionuse");
     *
     * @param  array @parameters  Array with strings representing
     *                            parameters to parse
     * @return array  parameter array from last response
     * @access public
     */
    function getParameters($parameters)
    {
      if ($parameters == null) {
        $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
      }
      $param_array = array();
      foreach ($parameters as $param) {
        if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
          return FALSE; //PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
        }
        $param_array[$param]=$out[1];
      }
      return $param_array;
    }

    /**
     * Verify Yubico OTP against multiple URLs
     * Protocol specification 2.0 is used to construct validation requests
     *
     * @param string $token        Yubico OTP
     * @param int $use_timestamp   1=>send request with &timestamp=1 to
     *                             get timestamp and session information
     *                             in the response
     * @param boolean $wait_for_all  If true, wait until all
     *                               servers responds (for debugging)
     * @param string $sl           Sync level in percentage between 0
     *                             and 100 or "fast" or "secure".
     * @param int $timeout         Max number of seconds to wait
     *                             for responses
     * @return mixed               PEAR error on error, true otherwise
     * @access public
     */
    function verify($token, $use_timestamp=null, $wait_for_all=False,
            $sl=null, $timeout=null)
    {
      /* Construct parameters string */
      $ret = $this->parsePasswordOTP($token);
      if (!$ret) {
        return FALSE; //Could not parse Yubikey OTP
      }
      $params = array('id'=>$this->_id, 
              'otp'=>$ret['otp'],
              'nonce'=>md5(uniqid(rand())));
      /* Take care of protocol version 2 parameters */
      if ($use_timestamp) $params['timestamp'] = 1;
      if ($sl) $params['sl'] = $sl;
      if ($timeout) $params['timeout'] = $timeout;
      ksort($params);
      $parameters = '';
      foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
      $parameters = ltrim($parameters, "&");

      /* Generate signature. */
      if($this->_key <> "") {
        $signature = base64_encode(hash_hmac('sha1', $parameters,
                         $this->_key, true));
        $signature = preg_replace('/\+/', '%2B', $signature);
        $parameters .= '&h=' . $signature;
      }

      /* Generate and prepare request. */
      $this->_lastquery=null;
      $this->URLreset();
      $mh = curl_multi_init();
      $ch = array();
      while($URLpart=$this->getNextURLpart()) 
        {
          /* Support https. */
          if ($this->_https) {
        $query = "https://";
          } else {
        $query = "http://";
          }
          $query .= $URLpart . "?" . $parameters;

          if ($this->_lastquery) { $this->_lastquery .= " "; }
          $this->_lastquery .= $query;

          $handle = curl_init($query);
          curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
          curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
          if (!$this->_httpsverify) {
        curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
          }
          curl_setopt($handle, CURLOPT_FAILONERROR, true);
          /* If timeout is set, we better apply it here as well
             in case the validation server fails to follow it. 
          */ 
          if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
          curl_multi_add_handle($mh, $handle);

          $ch[$handle] = $handle;
        }

      /* Execute and read request. */
      $this->_response=null;
      $replay=False;
      $valid=False;
      do {
        /* Let curl do its work. */
        while (($mrc = curl_multi_exec($mh, $active))
           == CURLM_CALL_MULTI_PERFORM)
          ;

        while ($info = curl_multi_info_read($mh)) {
          if ($info['result'] == CURLE_OK) {

        /* We have a complete response from one server. */

        $str = curl_multi_getcontent($info['handle']);
        $cinfo = curl_getinfo ($info['handle']);

        if ($wait_for_all) { # Better debug info
          $this->_response .= 'URL=' . $cinfo['url'] ."\n"
            . $str . "\n";
        }

        if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
          $status = $out[1];

          /* 
           * There are 3 cases.
           *
           * 1. OTP or Nonce values doesn't match - ignore
           * response.
           *
           * 2. We have a HMAC key.  If signature is invalid -
           * ignore response.  Return if status=OK or
           * status=REPLAYED_OTP.
           *
           * 3. Return if status=OK or status=REPLAYED_OTP.
           */
          if (!preg_match("/otp=".$params['otp']."/", $str) ||
              !preg_match("/nonce=".$params['nonce']."/", $str)) {
            /* Case 1. Ignore response. */
          } 
          elseif ($this->_key <> "") {
            /* Case 2. Verify signature first */
            $rows = explode("\r\n", $str);
            $response=array();
            while (list($key, $val) = each($rows)) {
              /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
              $val = preg_replace('/=/', '#', $val, 1);
              $row = explode("#", $val);

              if (empty($row[0]) && !isset($row[1]))
                continue; //weird bug, this fixes it.
              $response[$row[0]] = $row[1];
            }

            $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
            sort($parameters);
            $check=Null;
            foreach ($parameters as $param) {
              if (isset($response[$param]) && $response[$param]!=null) {
            if ($check) $check = $check . '&';
            $check = $check . $param . '=' . $response[$param];
              }
            }

            $checksignature =
              base64_encode(hash_hmac('sha1', utf8_encode($check),
                          $this->_key, true));

            if($response["h"] == $checksignature) {
              if ($status == 'REPLAYED_OTP') {
            if (!$wait_for_all) { $this->_response = $str; }
            $replay=True;
              } 
              if ($status == 'OK') {
            if (!$wait_for_all) { $this->_response = $str; }
            $valid=True;
              }
            }
          } else {
            /* Case 3. We check the status directly */
            if ($status == 'REPLAYED_OTP') {
              if (!$wait_for_all) { $this->_response = $str; }
              $replay=True;
            } 
            if ($status == 'OK') {
              if (!$wait_for_all) { $this->_response = $str; }
              $valid=True;
            }
          }
        }
        if (!$wait_for_all && ($valid || $replay)) 
          {
            /* We have status=OK or status=REPLAYED_OTP, return. */
            foreach ($ch as $h) {
              curl_multi_remove_handle($mh, $h);
              curl_close($h);
            }
            curl_multi_close($mh);
            if ($replay) return FALSE; //REPLAYED_OTP
            if ($valid) return true;
            return FALSE; //PEAR::raiseError($status);
          }

        curl_multi_remove_handle($mh, $info['handle']);
        curl_close($info['handle']);
        unset ($ch[$info['handle']]);
          }
          curl_multi_select($mh);
        }
      } while ($active);

      /* Typically this is only reached for wait_for_all=true or
       * when the timeout is reached and there is no
       * OK/REPLAYED_REQUEST answer (think firewall).
       */

      foreach ($ch as $h) {
        curl_multi_remove_handle ($mh, $h);
        curl_close ($h);
      }
      curl_multi_close ($mh);

      if ($replay) return FALSE; //PEAR::raiseError('REPLAYED_OTP');
      if ($valid) return true;
      return FALSE; //PEAR::raiseError('NO_VALID_ANSWER');
    }
}
?>

config/yubikey.php

<?php
// Goto https://upgrade.yubico.com/getapikey/ to get below values
$config["cid"] = "";
$config["key"] = "";

$config["apiurl"] = "api.yubico.com/wsapi/verify";
?>

(例子) controllers/verifyYubikey.php

class verifyyubikey extends CI_Controller
{
    function index ()
    {
        $this->load->helper('form');

        echo form_open(current_url());
        echo form_input('key');
        //echo form_submit('Submit', 'submit');
        echo form_close();

        $this->load->library('yubikey', array());

        $pkey = $this->yubikey->parsePasswordOTP($this->input->post('key'));
        $pkey = $pkey["prefix"];

        $out = $this->yubikey->verify($this->input->post('key'));

        echo ($out && $pkey == "someSavedPrefix")?"Verified":"Not valid";
    }
}

答案 1 :(得分:1)

我自己刚刚买了一个Yubikey,发现这个非常有用而且杂乱无章:

http://code.google.com/p/yubikey-php-webservice-class/downloads/detail?name=yubikeyPHPclass-0.96.tar.bz2&can=2&q=

下载并阅读。它需要PHP 5,这是我唯一的问题,因为我的服务器目前正在运行v4,虽然因为我只是在PHP 5服务器上临时存放来自.RAR文件的实际Yubi代码而指向它用于验证。

在我自己的代码中,我需要在需要用户登录的文件中执行的操作包含在PHP文件的顶部:

include  "yubisession.php";

然后,一旦Yubikey被验证,密钥的实际ID就是我接受的,所以它会创建一个会话,所以未来的scrips运行会看到set session并允许访问。

“yubisession.php”文件里面有这个 - 再次只是为了让你查看,如果你希望它立即起作用,它会需要重新编码:

$yubiuser       = "cccccccvvdkd";                               //  Yubikey User ID
$yubichkurl     = "http://www.pathto/yubicheck.php";                        //  Where the PHP 5 checker is
$callingpage        = "http://" . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'];           //  So we know where to go back to
session_start();
if($_SESSION['yubiok'] !== $yubiuser) {                                 //  If SESSION NOT Set
    if ($_POST['yubiok'] == $yubiuser)                              //  If SESSION NOT set Check if POST = user
        {
            $_SESSION['yubiok']     = $yubiuser;                            //  If SESSION NOT Set and POST DOES = user, set session =
            header("Location: $callingpage");                           //  Direct back into the script with the SESSION now set
        ob_end_flush();
        exit();
    }   
?>
...login yubikey stuff here that StackOverflow DOESNT like displaying...
<?
exit;
}