Add methods that throw an exception in case a token is invalid.
authorGunnar Wrobel <p@rdus.de>
Tue, 30 Nov 2010 12:37:53 +0000 (13:37 +0100)
committerGunnar Wrobel <p@rdus.de>
Tue, 30 Nov 2010 12:48:29 +0000 (13:48 +0100)
framework/Token/lib/Horde/Token/Base.php
framework/Token/lib/Horde/Token/Exception.php
framework/Token/lib/Horde/Token/Exception/Expired.php [new file with mode: 0644]
framework/Token/lib/Horde/Token/Exception/Invalid.php [new file with mode: 0644]
framework/Token/lib/Horde/Token/Exception/Used.php [new file with mode: 0644]
framework/Token/package.xml
framework/Token/test/Horde/Token/Unit/FileTest.php

index 9cbf433..91b8347 100644 (file)
@@ -137,18 +137,14 @@ abstract class Horde_Token_Base
      */
     public function validate($token, $seed = '', $timeout = null, $unique = false)
     {
-        $b = Horde_Url::uriB64Decode($token);
-        $nonce = substr($b, 0, 6);
-        $hash = substr($b, 6);
+        list($nonce, $hash) = $this->_decode($token);
         if ($hash != $this->_hash($nonce . $seed)) {
             return false;
         }
-        $timestamp = unpack('N', substr($nonce, 0, 4));
-        $timestamp = array_pop($timestamp);
         if ($timeout === null) {
             $timeout = $this->_params['token_lifetime'];
         }
-        if ($timeout >= 0 && (time() - $timestamp - $timeout) >= 0) {
+        if ($this->_isExpired($nonce, $timeout)) {
             return false;
         }
         if ($unique) {
@@ -157,6 +153,84 @@ abstract class Horde_Token_Base
         return true;
     }
 
+    /**
+     * Is the given token still valid? Throws an exception in case it is not.
+     *
+     * @param string  $token  The signed token.
+     * @param string  $seed   The unique ID of the token.
+nce?
+     *
+     * @return array An array of two elements: The nonce and the hash.
+     *
+     * @throws Horde_Token_Exception If the token was invalid.
+     */
+    public function isValid($token, $seed = '')
+    {
+        list($nonce, $hash) = $this->_decode($token);
+        if ($hash != $this->_hash($nonce . $seed)) {
+            throw new Horde_Token_Exception_Invalid('We cannot verify that this request was really sent by you. It could be a malicious request. If you intended to perform this action, you can retry it now.');
+        }
+        if ($this->_isExpired($nonce, $this->_params['token_lifetime'])) {
+            throw new Horde_Token_Exception_Expired(sprintf("This request cannot be completed because the link you followed or the form you submitted was only valid for %s minutes. Please try again now.", floor($this->_params['token_lifetime'] / 60)));
+        }
+        return array($nonce, $hash);
+    }
+
+    /**
+     * Is the given token valid and has never been used before? Throws an
+     * exception otherwise.
+     *
+     * @param string  $token  The signed token.
+     * @param string  $seed   The unique ID of the token.
+nce?
+     *
+     * @return NULL
+     *
+     * @throws Horde_Token_Exception If the token was invalid or has been used before.
+     */
+    public function isValidAndUnused($token, $seed = '')
+    {
+        list($nonce, $hash) = $this->isValid($token, $seed);
+        if (!$this->verify($nonce)) {
+            throw new Horde_Token_Exception_Used('This token has been used before!');
+        }
+    }
+
+    /**
+     * Decode a token into the prefixed nonce and the hash.
+     *
+     * @param string $token The token to be decomposed.
+     *
+     * @return array An array of two elements: The nonce and the hash.
+     */
+    private function _decode($token)
+    {
+        $b = Horde_Url::uriB64Decode($token);
+        return array(substr($b, 0, 6), substr($b, 6));
+    }
+
+    /**
+     * Has the nonce expired?
+     *
+     * @param string $nonce   The to be checked for expiration.
+     * @param int    $timeout The timeout that should be applied.
+     *
+     * @return boolean True if the nonce expired.
+     */
+    private function _isExpired($nonce, $timeout)
+    {
+        $timestamp = unpack('N', substr($nonce, 0, 4));
+        $timestamp = array_pop($timestamp);
+        return $timeout >= 0 && (time() - $timestamp - $timeout) >= 0;
+    }
+
+    /**
+     * Sign the given text with the secret.
+     *
+     * @param string $text The text to be signed.
+     *
+     * @return string The hashed text.
+     */
     private function _hash($text)
     {
         return hash('sha256', $text . $this->_params['secret'], true);
index 97cedc0..f7bde40 100644 (file)
@@ -11,6 +11,6 @@
  * @category Horde
  * @package  Token
  */
-class Horde_Token_Exception extends Horde_Exception_Prior
+class Horde_Token_Exception extends Horde_Exception
 {
 }
diff --git a/framework/Token/lib/Horde/Token/Exception/Expired.php b/framework/Token/lib/Horde/Token/Exception/Expired.php
new file mode 100644 (file)
index 0000000..dec3b9a
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Indicates an expired token.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+
+/**
+ * Indicates an expired token.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+class Horde_Token_Exception_Expired extends Horde_Token_Exception
+{
+}
diff --git a/framework/Token/lib/Horde/Token/Exception/Invalid.php b/framework/Token/lib/Horde/Token/Exception/Invalid.php
new file mode 100644 (file)
index 0000000..1357ef1
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Indicates an invalid token.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+
+/**
+ * Indicates an invalid token.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+class Horde_Token_Exception_Invalid extends Horde_Token_Exception
+{
+}
diff --git a/framework/Token/lib/Horde/Token/Exception/Used.php b/framework/Token/lib/Horde/Token/Exception/Used.php
new file mode 100644 (file)
index 0000000..f4f3190
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Indicates a used token.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+
+/**
+ * Indicates a used token.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Token
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Token
+ */
+class Horde_Token_Exception_Used extends Horde_Token_Exception
+{
+}
index 2b16147..d1a7063 100644 (file)
@@ -1,22 +1,19 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
-http://pear.php.net/dtd/tasks-1.0.xsd
-http://pear.php.net/dtd/package-2.0
-http://pear.php.net/dtd/package-2.0.xsd">
+<package packagerversion="1.9.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
  <name>Token</name>
  <channel>pear.horde.org</channel>
  <summary>Horde Token API</summary>
  <description>The Horde_Token:: class provides a common abstracted interface
  into the various token generation mediums. It also includes all of the
- functions for retrieving, storing, and checking tokens.
- </description>
+ functions for retrieving, storing, and checking tokens.</description>
  <lead>
   <name>Chuck Hagenbuch</name>
   <user>chuck</user>
   <email>chuck@horde.org</email>
   <active>yes</active>
  </lead>
- <date>2009-02-21</date>
+ <date>2010-11-30</date>
+ <time>13:33:08</time>
  <version>
   <release>0.1.0</release>
   <api>0.1.0</api>
@@ -26,16 +23,22 @@ http://pear.php.net/dtd/package-2.0.xsd">
   <api>beta</api>
  </stability>
  <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
- <notes>* Add Horde_Token_Exception::.
+ <notes>
+* Add Horde_Token_Exception::.
  * Move driver code into sub-classes.
  * Use exceptions, not PEAR_Errors.
  * Initial Horde 4 package.
  </notes>
  <contents>
-  <dir name="/">
+  <dir baseinstalldir="/" name="/">
    <dir name="lib">
     <dir name="Horde">
      <dir name="Token">
+      <dir name="Exception">
+       <file name="Expired.php" role="php" />
+       <file name="Invalid.php" role="php" />
+       <file name="Used.php" role="php" />
+      </dir> <!-- /lib/Horde/Token/Exception -->
       <file name="Base.php" role="php" />
       <file name="Exception.php" role="php" />
       <file name="File.php" role="php" />
@@ -45,6 +48,18 @@ http://pear.php.net/dtd/package-2.0.xsd">
      <file name="Token.php" role="php" />
     </dir> <!-- /lib/Horde -->
    </dir> <!-- /lib -->
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Token">
+      <dir name="Unit">
+       <file name="FileTest.php" role="test" />
+      </dir> <!-- /test/Horde/Token/Unit -->
+      <file name="AllTests.php" role="test" />
+      <file name="Autoload.php" role="test" />
+      <file name="phpunit.xml" role="test" />
+     </dir> <!-- /test/Horde/Token -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
   </dir> <!-- / -->
  </contents>
  <dependencies>
@@ -81,28 +96,50 @@ http://pear.php.net/dtd/package-2.0.xsd">
  </dependencies>
  <phprelease>
   <filelist>
-   <install name="lib/Horde/Token/Base.php" as="Horde/Token/Base.php" />
-   <install name="lib/Horde/Token/Exception.php" as="Horde/Token/Exception.php" />
-   <install name="lib/Horde/Token/File.php" as="Horde/Token/File.php" />
-   <install name="lib/Horde/Token/Null.php" as="Horde/Token/Null.php" />
-   <install name="lib/Horde/Token/Sql.php" as="Horde/Token/Sql.php" />
-   <install name="lib/Horde/Token.php" as="Horde/Token.php" />
+   <install as="Horde/Token.php" name="lib/Horde/Token.php" />
+   <install as="Horde/Token/Base.php" name="lib/Horde/Token/Base.php" />
+   <install as="Horde/Token/Exception.php" name="lib/Horde/Token/Exception.php" />
+   <install as="Horde/Token/File.php" name="lib/Horde/Token/File.php" />
+   <install as="Horde/Token/Null.php" name="lib/Horde/Token/Null.php" />
+   <install as="Horde/Token/Sql.php" name="lib/Horde/Token/Sql.php" />
+   <install as="Horde/Token/Exception/Expired.php" name="lib/Horde/Token/Exception/Expired.php" />
+   <install as="Horde/Token/Exception/Invalid.php" name="lib/Horde/Token/Exception/Invalid.php" />
+   <install as="Horde/Token/Exception/Used.php" name="lib/Horde/Token/Exception/Used.php" />
+   <install as="Horde/Token/AllTests.php" name="test/Horde/Token/AllTests.php" />
+   <install as="Horde/Token/Autoload.php" name="test/Horde/Token/Autoload.php" />
+   <install as="Horde/Token/phpunit.xml" name="test/Horde/Token/phpunit.xml" />
+   <install as="Horde/Token/Unit/FileTest.php" name="test/Horde/Token/Unit/FileTest.php" />
   </filelist>
  </phprelease>
  <changelog>
   <release>
    <version>
-    <release>0.0.4</release>
-    <api>0.0.4</api>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2003-07-03</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>
+Initial release as a PEAR package
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
    </version>
    <stability>
     <release>beta</release>
     <api>beta</api>
    </stability>
-   <date>2006-05-08</date>
-   <time>23:48:27</time>
+   <date>2004-11-23</date>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>Converted to package.xml 2.0 for pear.horde.org
+   <notes>
+Remove assumption of IPv4 IP address format
    </notes>
   </release>
   <release>
@@ -116,35 +153,42 @@ http://pear.php.net/dtd/package-2.0.xsd">
    </stability>
    <date>2005-01-01</date>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>* Add support for separate read and write DB servers for the sql driver.
+   <notes>
+* Add support for separate read and write DB servers for the sql driver.
    </notes>
   </release>
   <release>
    <version>
-    <release>0.0.2</release>
-    <api>0.0.2</api>
+    <release>0.0.4</release>
+    <api>0.0.4</api>
    </version>
    <stability>
     <release>beta</release>
     <api>beta</api>
    </stability>
-   <date>2004-11-23</date>
+   <date>2006-05-08</date>
+   <time>23:48:27</time>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>Remove assumption of IPv4 IP address format
+   <notes>
+Converted to package.xml 2.0 for pear.horde.org
    </notes>
   </release>
   <release>
    <version>
-    <release>0.0.1</release>
-    <api>0.0.1</api>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
    </version>
    <stability>
-    <release>alpha</release>
-    <api>alpha</api>
+    <release>beta</release>
+    <api>beta</api>
    </stability>
-   <date>2003-07-03</date>
+   <date>2010-11-30</date>
    <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
-   <notes>Initial release as a PEAR package
+   <notes>
+* Add Horde_Token_Exception::.
+ * Move driver code into sub-classes.
+ * Use exceptions, not PEAR_Errors.
+ * Initial Horde 4 package.
    </notes>
   </release>
  </changelog>
index 81929f3..e0d93c8 100644 (file)
@@ -17,7 +17,7 @@
 require_once dirname(__FILE__) . '/../Autoload.php';
 
 /**
- * Base for session testing.
+ * Test the file based token backend.
  *
  * Copyright 2010 The Horde Project (http://www.horde.org/)
  *
@@ -133,6 +133,56 @@ class Horde_Token_Unit_FileTest extends PHPUnit_Framework_TestCase
     }
 
     /**
+     * @expectedException Horde_Token_Exception_Invalid
+     */
+    public function testInvalidTokenException()
+    {
+        $t = new Horde_Token_File(array('secret' => 'abc'));
+        $t->isValid('something');
+    }
+
+    /**
+     * @expectedException Horde_Token_Exception_Invalid
+     */
+    public function testInvalidSeedException()
+    {
+        $t = new Horde_Token_File(array('secret' => 'abc'));
+        $t->isValid($t->get('a'), 'b');
+    }
+
+    /**
+     * @expectedException Horde_Token_Exception_Expired
+     */
+    public function testTimeoutException()
+    {
+        $t = new Horde_Token_File(
+            array(
+                'secret' => 'abc',
+                'token_lifetime' => 1
+            )
+        );
+        $token = $t->get('a');
+        sleep(1);
+        $t->isValid($token, 'a');
+    }
+
+    /**
+     * @expectedException Horde_Token_Exception_Used
+     */
+    public function testIsValidAndUnusedException()
+    {
+        $t = new Horde_Token_File(
+            array(
+                'secret' => 'abc',
+                'token_dir' => $this->_getTemporaryDirectory()
+            )
+        );
+        $token = $t->get('a');
+        $t->isValidAndUnused($token, 'a');
+        $t->isValidAndUnused($token, 'a');
+    }
+
+    /**
      * @expectedException Horde_Token_Exception
      */
     public function testInvalidConstruction()