4 * This module contains code for dealing with associations between
5 * consumers and servers.
9 * LICENSE: See the COPYING file included in this distribution.
12 * @author JanRain, Inc. <openid@janrain.com>
13 * @copyright 2005-2008 Janrain, Inc.
14 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
20 require_once 'Auth/OpenID/CryptUtil.php';
25 require_once 'Auth/OpenID/KVForm.php';
30 require_once 'Auth/OpenID/HMAC.php';
33 * This class represents an association between a server and a
34 * consumer. In general, users of this library will never see
35 * instances of this object. The only exception is if you implement a
36 * custom {@link Auth_OpenID_OpenIDStore}.
38 * If you do implement such a store, it will need to store the values
39 * of the handle, secret, issued, lifetime, and assoc_type instance
44 class Auth_OpenID_Association {
47 * This is a HMAC-SHA1 specific value.
54 * The ordering and name of keys as stored by serialize.
58 var $assoc_keys = array(
68 'HMAC-SHA1' => 'Auth_OpenID_HMACSHA1',
69 'HMAC-SHA256' => 'Auth_OpenID_HMACSHA256'
73 * This is an alternate constructor (factory method) used by the
74 * OpenID consumer library to create associations. OpenID store
75 * implementations shouldn't use this constructor.
79 * @param integer $expires_in This is the amount of time this
80 * association is good for, measured in seconds since the
81 * association was issued.
83 * @param string $handle This is the handle the server gave this
86 * @param string secret This is the shared secret the server
87 * generated for this association.
89 * @param assoc_type This is the type of association this
90 * instance represents. The only valid values of this field at
91 * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
92 * be defined in the future.
94 * @return association An {@link Auth_OpenID_Association}
97 static function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
100 $lifetime = $expires_in;
101 return new Auth_OpenID_Association($handle, $secret,
102 $issued, $lifetime, $assoc_type);
106 * This is the standard constructor for creating an association.
107 * The library should create all of the necessary associations, so
108 * this constructor is not part of the external API.
112 * @param string $handle This is the handle the server gave this
115 * @param string $secret This is the shared secret the server
116 * generated for this association.
118 * @param integer $issued This is the time this association was
119 * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a
122 * @param integer $lifetime This is the amount of time this
123 * association is good for, measured in seconds since the
124 * association was issued.
126 * @param string $assoc_type This is the type of association this
127 * instance represents. The only valid values of this field at
128 * this time is 'HMAC-SHA1' and 'HMAC-SHA256', but new types may
129 * be defined in the future.
131 function Auth_OpenID_Association(
132 $handle, $secret, $issued, $lifetime, $assoc_type)
134 if (!in_array($assoc_type,
135 Auth_OpenID_getSupportedAssociationTypes(), true)) {
136 $fmt = 'Unsupported association type (%s)';
137 trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
140 $this->handle = $handle;
141 $this->secret = $secret;
142 $this->issued = $issued;
143 $this->lifetime = $lifetime;
144 $this->assoc_type = $assoc_type;
148 * This returns the number of seconds this association is still
149 * valid for, or 0 if the association is no longer valid.
151 * @return integer $seconds The number of seconds this association
152 * is still valid for, or 0 if the association is no longer valid.
154 function getExpiresIn($now = null)
160 return max(0, $this->issued + $this->lifetime - $now);
164 * This checks to see if two {@link Auth_OpenID_Association}
165 * instances represent the same association.
167 * @return bool $result true if the two instances represent the
168 * same association, false otherwise.
170 function equal($other)
172 return ((gettype($this) == gettype($other))
173 && ($this->handle == $other->handle)
174 && ($this->secret == $other->secret)
175 && ($this->issued == $other->issued)
176 && ($this->lifetime == $other->lifetime)
177 && ($this->assoc_type == $other->assoc_type));
181 * Convert an association to KV form.
183 * @return string $result String in KV form suitable for
184 * deserialization by deserialize.
190 'handle' => $this->handle,
191 'secret' => base64_encode($this->secret),
192 'issued' => strval(intval($this->issued)),
193 'lifetime' => strval(intval($this->lifetime)),
194 'assoc_type' => $this->assoc_type
197 assert(array_keys($data) == $this->assoc_keys);
199 return Auth_OpenID_KVForm::fromArray($data, $strict = true);
203 * Parse an association as stored by serialize(). This is the
204 * inverse of serialize.
206 * @param string $assoc_s Association as serialized by serialize()
207 * @return Auth_OpenID_Association $result instance of this class
209 static function deserialize($class_name, $assoc_s)
211 $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
214 foreach ($pairs as $key => $value) {
215 if (is_array($value)) {
216 list($key, $value) = $value;
222 $class_vars = get_class_vars($class_name);
223 $class_assoc_keys = $class_vars['assoc_keys'];
226 sort($class_assoc_keys);
228 if ($keys != $class_assoc_keys) {
229 trigger_error('Unexpected key values: ' . var_export($keys, true),
234 $version = $pairs['version'];
235 $handle = $pairs['handle'];
236 $secret = $pairs['secret'];
237 $issued = $pairs['issued'];
238 $lifetime = $pairs['lifetime'];
239 $assoc_type = $pairs['assoc_type'];
241 if ($version != '2') {
242 trigger_error('Unknown version: ' . $version, E_USER_WARNING);
246 $issued = intval($issued);
247 $lifetime = intval($lifetime);
248 $secret = base64_decode($secret);
250 return new $class_name(
251 $handle, $secret, $issued, $lifetime, $assoc_type);
255 * Generate a signature for a sequence of (key, value) pairs
258 * @param array $pairs The pairs to sign, in order. This is an
259 * array of two-tuples.
260 * @return string $signature The binary signature of this sequence
263 function sign($pairs)
265 $kv = Auth_OpenID_KVForm::fromArray($pairs);
267 /* Invalid association types should be caught at constructor */
268 $callback = $this->_macs[$this->assoc_type];
270 return call_user_func_array($callback, array($this->secret, $kv));
274 * Generate a signature for some fields in a dictionary
277 * @param array $fields The fields to sign, in order; this is an
279 * @param array $data Dictionary of values to sign (an array of
280 * string => string pairs).
281 * @return string $signature The signature, base64 encoded
283 function signMessage($message)
285 if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') ||
286 $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) {
291 $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS,
294 if ($extant_handle && ($extant_handle != $this->handle)) {
295 // raise ValueError("Message has a different association handle")
299 $signed_message = $message;
300 $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle',
303 $message_keys = array_keys($signed_message->toPostArgs());
304 $signed_list = array();
305 $signed_prefix = 'openid.';
307 foreach ($message_keys as $k) {
308 if (strpos($k, $signed_prefix) === 0) {
309 $signed_list[] = substr($k, strlen($signed_prefix));
313 $signed_list[] = 'signed';
316 $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed',
317 implode(',', $signed_list));
318 $sig = $this->getMessageSignature($signed_message);
319 $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig);
320 return $signed_message;
324 * Given a {@link Auth_OpenID_Message}, return the key/value pairs
325 * to be signed according to the signed list in the message. If
326 * the message lacks a signed list, return null.
330 function _makePairs($message)
332 $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed');
333 if (!$signed || Auth_OpenID::isFailure($signed)) {
334 // raise ValueError('Message has no signed list: %s' % (message,))
338 $signed_list = explode(',', $signed);
340 $data = $message->toPostArgs();
341 foreach ($signed_list as $field) {
342 $pairs[] = array($field, Auth_OpenID::arrayGet($data,
350 * Given an {@link Auth_OpenID_Message}, return the signature for
351 * the signed list in the message.
355 function getMessageSignature($message)
357 $pairs = $this->_makePairs($message);
358 return base64_encode($this->sign($pairs));
362 * Confirm that the signature of these fields matches the
363 * signature contained in the data.
367 function checkMessageSignature($message)
369 $sig = $message->getArg(Auth_OpenID_OPENID_NS,
372 if (!$sig || Auth_OpenID::isFailure($sig)) {
376 $calculated_sig = $this->getMessageSignature($message);
377 return Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig);
381 function Auth_OpenID_getSecretSize($assoc_type)
383 if ($assoc_type == 'HMAC-SHA1') {
385 } else if ($assoc_type == 'HMAC-SHA256') {
392 function Auth_OpenID_getAllAssociationTypes()
394 return array('HMAC-SHA1', 'HMAC-SHA256');
397 function Auth_OpenID_getSupportedAssociationTypes()
399 $a = array('HMAC-SHA1');
401 if (Auth_OpenID_HMACSHA256_SUPPORTED) {
402 $a[] = 'HMAC-SHA256';
408 function Auth_OpenID_getSessionTypes($assoc_type)
410 $assoc_to_session = array(
411 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption'));
413 if (Auth_OpenID_HMACSHA256_SUPPORTED) {
414 $assoc_to_session['HMAC-SHA256'] =
415 array('DH-SHA256', 'no-encryption');
418 return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array());
421 function Auth_OpenID_checkSessionType($assoc_type, $session_type)
423 if (!in_array($session_type,
424 Auth_OpenID_getSessionTypes($assoc_type))) {
431 function Auth_OpenID_getDefaultAssociationOrder()
435 if (!Auth_OpenID_noMathSupport()) {
436 $order[] = array('HMAC-SHA1', 'DH-SHA1');
438 if (Auth_OpenID_HMACSHA256_SUPPORTED) {
439 $order[] = array('HMAC-SHA256', 'DH-SHA256');
443 $order[] = array('HMAC-SHA1', 'no-encryption');
445 if (Auth_OpenID_HMACSHA256_SUPPORTED) {
446 $order[] = array('HMAC-SHA256', 'no-encryption');
452 function Auth_OpenID_getOnlyEncryptedOrder()
456 foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) {
457 list($assoc, $session) = $pair;
459 if ($session != 'no-encryption') {
460 if (Auth_OpenID_HMACSHA256_SUPPORTED &&
461 ($assoc == 'HMAC-SHA256')) {
463 } else if ($assoc != 'HMAC-SHA256') {
472 function Auth_OpenID_getDefaultNegotiator()
474 return new Auth_OpenID_SessionNegotiator(
475 Auth_OpenID_getDefaultAssociationOrder());
478 function Auth_OpenID_getEncryptedNegotiator()
480 return new Auth_OpenID_SessionNegotiator(
481 Auth_OpenID_getOnlyEncryptedOrder());
485 * A session negotiator controls the allowed and preferred association
486 * types and association session types. Both the {@link
487 * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use
488 * negotiators when creating associations.
490 * You can create and use negotiators if you:
492 * - Do not want to do Diffie-Hellman key exchange because you use
493 * transport-layer encryption (e.g. SSL)
495 * - Want to use only SHA-256 associations
497 * - Do not want to support plain-text associations over a non-secure
500 * It is up to you to set a policy for what kinds of associations to
501 * accept. By default, the library will make any kind of association
502 * that is allowed in the OpenID 2.0 specification.
504 * Use of negotiators in the library
505 * =================================
507 * When a consumer makes an association request, it calls {@link
508 * getAllowedType} to get the preferred association type and
509 * association session type.
511 * The server gets a request for a particular association/session type
512 * and calls {@link isAllowed} to determine if it should create an
513 * association. If it is supported, negotiation is complete. If it is
514 * not, the server calls {@link getAllowedType} to get an allowed
515 * association type to return to the consumer.
517 * If the consumer gets an error response indicating that the
518 * requested association/session type is not supported by the server
519 * that contains an assocation/session type to try, it calls {@link
520 * isAllowed} to determine if it should try again with the given
521 * combination of association/session type.
525 class Auth_OpenID_SessionNegotiator {
526 function Auth_OpenID_SessionNegotiator($allowed_types)
528 $this->allowed_types = array();
529 $this->setAllowedTypes($allowed_types);
533 * Set the allowed association types, checking to make sure each
534 * combination is valid.
538 function setAllowedTypes($allowed_types)
540 foreach ($allowed_types as $pair) {
541 list($assoc_type, $session_type) = $pair;
542 if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
547 $this->allowed_types = $allowed_types;
552 * Add an association type and session type to the allowed types
553 * list. The assocation/session pairs are tried in the order that
558 function addAllowedType($assoc_type, $session_type = null)
560 if ($this->allowed_types === null) {
561 $this->allowed_types = array();
564 if ($session_type === null) {
565 $available = Auth_OpenID_getSessionTypes($assoc_type);
571 foreach ($available as $session_type) {
572 $this->addAllowedType($assoc_type, $session_type);
575 if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) {
576 $this->allowed_types[] = array($assoc_type, $session_type);
585 // Is this combination of association type and session type allowed?
586 function isAllowed($assoc_type, $session_type)
588 $assoc_good = in_array(array($assoc_type, $session_type),
589 $this->allowed_types);
591 $matches = in_array($session_type,
592 Auth_OpenID_getSessionTypes($assoc_type));
594 return ($assoc_good && $matches);
598 * Get a pair of assocation type and session type that are
601 function getAllowedType()
603 if (!$this->allowed_types) {
604 return array(null, null);
607 return $this->allowed_types[0];