1: <?php
2: /**
3: * The Horde_Token_Base:: class provides a common abstracted interface for
4: * a token implementation.
5: *
6: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10: *
11: * @author Max Kalika <max@horde.org>
12: * @author Chuck Hagenbuch <chuck@horde.org>
13: * @category Horde
14: * @package Token
15: */
16: abstract class Horde_Token_Base
17: {
18: /**
19: * Hash of parameters necessary to use the chosen backend.
20: *
21: * @var array
22: */
23: protected $_params = array();
24:
25: /**
26: * Constructor.
27: *
28: * @param array $params Required parameters:
29: * - secret (string): The secret string used for signing tokens.
30: * Optional parameters:
31: * - token_lifetime (integer): The number of seconds after which tokens
32: * time out. Negative numbers represent "no
33: * timeout". The default is "-1".
34: * - timeout (integer): The period (in seconds) after which an id is purged.
35: * DEFAULT: 86400 (24 hours)
36: */
37: public function __construct($params)
38: {
39: if (!isset($params['secret'])) {
40: throw new Horde_Token_Exception('Missing secret parameter.');
41: }
42:
43: $params = array_merge(array(
44: 'token_lifetime' => -1,
45: 'timeout' => 86400
46: ), $params);
47:
48: $this->_params = $params;
49: }
50:
51: /**
52: * Checks if the given token has been previously used. First
53: * purges all expired tokens. Then retrieves current tokens for
54: * the given ip address. If the specified token was not found,
55: * adds it.
56: *
57: * @param string $token The value of the token to check.
58: *
59: * @return boolean True if the token has not been used, false otherwise.
60: * @throws Horde_Token_Exception
61: */
62: public function verify($token)
63: {
64: $this->purge();
65:
66: if ($this->exists($token)) {
67: return false;
68: }
69:
70: $this->add($token);
71: return true;
72: }
73:
74: /**
75: * Does the token exist?
76: *
77: * @param string $tokenID Token ID.
78: *
79: * @return boolean True if the token exists.
80: * @throws Horde_Token_Exception
81: */
82: abstract public function exists($tokenID);
83:
84: /**
85: * Add a token ID.
86: *
87: * @param string $tokenID Token ID to add.
88: *
89: * @throws Horde_Token_Exception
90: */
91: abstract public function add($tokenID);
92:
93: /**
94: * Delete all expired connection IDs.
95: *
96: * @throws Horde_Token_Exception
97: */
98: abstract public function purge();
99:
100: /**
101: * Return a new signed token.
102: *
103: * @param string $seed A unique ID to be included in the token.
104: *
105: * @return string The new token.
106: */
107: public function get($seed = '')
108: {
109: $nonce = $this->getNonce();
110: return Horde_Url::uriB64Encode(
111: $nonce . $this->_hash($nonce . $seed)
112: );
113: }
114:
115: /**
116: * Validate a signed token.
117: *
118: * @param string $token The signed token.
119: * @param string $seed The unique ID of the token.
120: * @param int $timeout Timout of the token in seconds.
121: * Values below zero represent no timeout.
122: * @param boolean $unique Should validation of the token succeed only once?
123: *
124: * @return boolean True if the token was valid.
125: */
126: public function isValid($token, $seed = '', $timeout = null,
127: $unique = false)
128: {
129: list($nonce, $hash) = $this->_decode($token);
130: if ($hash != $this->_hash($nonce . $seed)) {
131: return false;
132: }
133: if ($timeout === null) {
134: $timeout = $this->_params['token_lifetime'];
135: }
136: if ($this->_isExpired($nonce, $timeout)) {
137: return false;
138: }
139: if ($unique) {
140: return $this->verify($token);
141: }
142: return true;
143: }
144:
145: /**
146: * Is the given token still valid? Throws an exception in case it is not.
147: *
148: * @param string $token The signed token.
149: * @param string $seed The unique ID of the token.
150: * @param int $timeout Timout of the token in seconds.
151: * Values below zero represent no timeout.
152: *
153: * @return array An array of two elements: The nonce and the hash.
154: *
155: * @throws Horde_Token_Exception If the token was invalid.
156: */
157: public function validate($token, $seed = '', $timeout = null)
158: {
159: list($nonce, $hash) = $this->_decode($token);
160: if ($hash != $this->_hash($nonce . $seed)) {
161: throw new Horde_Token_Exception_Invalid(Horde_Token_Translation::t('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.'));
162: }
163: if ($timeout === null) {
164: $timeout = $this->_params['token_lifetime'];
165: }
166: if ($this->_isExpired($nonce, $timeout)) {
167: throw new Horde_Token_Exception_Expired(sprintf(Horde_Token_Translation::t("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($timeout / 60)));
168: }
169: return array($nonce, $hash);
170: }
171:
172: /**
173: * Is the given token valid and has never been used before? Throws an
174: * exception otherwise.
175: *
176: * @param string $token The signed token.
177: * @param string $seed The unique ID of the token.
178: *
179: * @return NULL
180: *
181: * @throws Horde_Token_Exception If the token was invalid or has been
182: * used before.
183: */
184: public function validateUnique($token, $seed = '')
185: {
186: if (!$this->isValid($token, $seed)) {
187: throw new Horde_Token_Exception_Used(Horde_Token_Translation::t('This token is invalid!'));
188: }
189:
190: if (!$this->verify($token)) {
191: throw new Horde_Token_Exception_Used(Horde_Token_Translation::t('This token has been used before!'));
192: }
193: }
194:
195: /**
196: * Decode a token into the prefixed nonce and the hash.
197: *
198: * @param string $token The token to be decomposed.
199: *
200: * @return array An array of two elements: The nonce and the hash.
201: */
202: private function _decode($token)
203: {
204: $b = Horde_Url::uriB64Decode($token);
205: return array(substr($b, 0, 6), substr($b, 6));
206: }
207:
208: /**
209: * Has the nonce expired?
210: *
211: * @param string $nonce The to be checked for expiration.
212: * @param int $timeout The timeout that should be applied.
213: *
214: * @return boolean True if the nonce expired.
215: */
216: private function _isExpired($nonce, $timeout)
217: {
218: $timestamp = unpack('N', substr($nonce, 0, 4));
219: $timestamp = array_pop($timestamp);
220: return $timeout >= 0 && (time() - $timestamp - $timeout) >= 0;
221: }
222:
223: /**
224: * Sign the given text with the secret.
225: *
226: * @param string $text The text to be signed.
227: *
228: * @return string The hashed text.
229: */
230: private function _hash($text)
231: {
232: return hash('sha256', $text . $this->_params['secret'], true);
233: }
234:
235: /**
236: * Return a "number used once" (a concatenation of a timestamp and a random
237: * numer).
238: *
239: * @return string A string of 6 bytes.
240: */
241: public function getNonce()
242: {
243: return pack('Nn', time(), mt_rand());
244: }
245:
246: /**
247: * Encodes the remote address.
248: *
249: * @return string Encoded address.
250: */
251: protected function _encodeRemoteAddress()
252: {
253: return isset($_SERVER['REMOTE_ADDR'])
254: ? base64_encode($_SERVER['REMOTE_ADDR'])
255: : '';
256: }
257: }
258: