I'm planning on adding multi-factor authentication for the admins to a site I'm working on. I decided to use Yubikey because of its simplicity, but am running into a roadblock on how to implement it.
Does anyone have a good example of how to save Yubikey, and how to verify an entered Yubikey (on login) is the saved Yubikey, and is a va开发者_JAVA技巧lid OTP? I saw an entire site example on the Yubikey website, but I can't seem to find the bits I'm looking for. I also don't want to use PEAR at all
Thanks
Well as Wesley pointed out, there doesn't seem to be a lot of knowledge about this product, so I took the time and modified the wonderful library put out on google code to work with codeigniter.
Sadly while doing this I had to remove the PEAR error messages, so errors now just return a generic FALSE. The class is below, I hope somebody finds it useful
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 ×tamp=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";
?>
(an example)
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";
}
}
I just bought a Yubikey myself and found this to be very helpful and clutter free:
http://code.google.com/p/yubikey-php-webservice-class/downloads/detail?name=yubikeyPHPclass-0.96.tar.bz2&can=2&q=
Download that and have a read. It requires PHP 5 and that was the only problem I had as my server is running v4 at the moment, not a big hassle though as I just temporarily housed the actual Yubi code from the .RAR file on a PHP 5 server and pointed to it for validation.
In my own code all I need to do in files that require a user to be logged in is include this at the top of the PHP file :
include "yubisession.php";
It then creates a session once a Yubikey has been authenticated and the key'd actual ID is one I accept so future scrips running see the set session and allow access.
The "yubisession.php" file has this inside - again just for you to view, it would requrire recoding if you wanted it to work right off the bat:
$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;
}
精彩评论