4 * Implements the OpenID attribute exchange specification, version 1.0
5 * as of svn revision 370 from openid.net svn.
11 * Require utility classes and functions for the consumer.
13 require_once "Auth/OpenID/Extension.php";
14 require_once "Auth/OpenID/Message.php";
15 require_once "Auth/OpenID/TrustRoot.php";
17 define('Auth_OpenID_AX_NS_URI',
18 'http://openid.net/srv/ax/1.0');
20 // Use this as the 'count' value for an attribute in a FetchRequest to
21 // ask for as many values as the OP can provide.
22 define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited');
24 // Minimum supported alias length in characters. Here for
26 define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32);
33 class Auth_OpenID_AX {
35 * @param mixed $thing Any object which may be an
36 * Auth_OpenID_AX_Error object.
38 * @return bool true if $thing is an Auth_OpenID_AX_Error; false
41 static function isError($thing)
43 return is_a($thing, 'Auth_OpenID_AX_Error');
48 * Check an alias for invalid characters; raise AXError if any are
49 * found. Return None if the alias is valid.
51 function Auth_OpenID_AX_checkAlias($alias)
53 if (strpos($alias, ',') !== false) {
54 return new Auth_OpenID_AX_Error(sprintf(
55 "Alias %s must not contain comma", $alias));
57 if (strpos($alias, '.') !== false) {
58 return new Auth_OpenID_AX_Error(sprintf(
59 "Alias %s must not contain period", $alias));
66 * Results from data that does not meet the attribute exchange 1.0
71 class Auth_OpenID_AX_Error {
72 function Auth_OpenID_AX_Error($message=null)
74 $this->message = $message;
79 * Abstract class containing common code for attribute exchange
84 class Auth_OpenID_AX_Message extends Auth_OpenID_Extension {
86 * ns_alias: The preferred namespace alias for attribute exchange
92 * mode: The type of this attribute exchange message. This must be
93 * overridden in subclasses.
97 var $ns_uri = Auth_OpenID_AX_NS_URI;
100 * Return Auth_OpenID_AX_Error if the mode in the attribute
101 * exchange arguments does not match what is expected for this
102 * class; true otherwise.
106 function _checkMode($ax_args)
108 $mode = Auth_OpenID::arrayGet($ax_args, 'mode');
109 if ($mode != $this->mode) {
110 return new Auth_OpenID_AX_Error(
112 "Expected mode '%s'; got '%s'",
113 $this->mode, $mode));
120 * Return a set of attribute exchange arguments containing the
121 * basic information that must be in every attribute exchange
128 return array('mode' => $this->mode);
133 * Represents a single attribute in an attribute exchange
134 * request. This should be added to an AXRequest object in order to
135 * request the attribute.
139 class Auth_OpenID_AX_AttrInfo {
141 * Construct an attribute information object. Do not call this
142 * directly; call make(...) instead.
144 * @param string $type_uri The type URI for this attribute.
146 * @param int $count The number of values of this type to request.
148 * @param bool $required Whether the attribute will be marked as
149 * required in the request.
151 * @param string $alias The name that should be given to this
152 * attribute in the request.
154 function Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
158 * required: Whether the attribute will be marked as required
159 * when presented to the subject of the attribute exchange
162 $this->required = $required;
165 * count: How many values of this type to request from the
166 * subject. Defaults to one.
168 $this->count = $count;
171 * type_uri: The identifier that determines what the attribute
172 * represents and how it is serialized. For example, one type
173 * URI representing dates could represent a Unix timestamp in
174 * base 10 and another could represent a human-readable
177 $this->type_uri = $type_uri;
180 * alias: The name that should be given to this attribute in
181 * the request. If it is not supplied, a generic name will be
182 * assigned. For example, if you want to call a Unix timestamp
183 * value 'tstamp', set its alias to that value. If two
184 * attributes in the same message request to use the same
185 * alias, the request will fail to be generated.
187 $this->alias = $alias;
191 * Construct an attribute information object. For parameter
192 * details, see the constructor.
194 static function make($type_uri, $count=1, $required=false,
197 if ($alias !== null) {
198 $result = Auth_OpenID_AX_checkAlias($alias);
200 if (Auth_OpenID_AX::isError($result)) {
205 return new Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
210 * When processing a request for this attribute, the OP should
211 * call this method to determine whether all available attribute
212 * values were requested. If self.count == UNLIMITED_VALUES, this
213 * returns True. Otherwise this returns False, in which case
214 * self.count is an integer.
216 function wantsUnlimitedValues()
218 return $this->count === Auth_OpenID_AX_UNLIMITED_VALUES;
223 * Given a namespace mapping and a string containing a comma-separated
224 * list of namespace aliases, return a list of type URIs that
225 * correspond to those aliases.
227 * @param $namespace_map The mapping from namespace URI to alias
228 * @param $alias_list_s The string containing the comma-separated
229 * list of aliases. May also be None for convenience.
231 * @return $seq The list of namespace URIs that corresponds to the
232 * supplied list of aliases. If the string was zero-length or None, an
233 * empty list will be returned.
235 * return null If an alias is present in the list of aliases but
236 * is not present in the namespace map.
238 function Auth_OpenID_AX_toTypeURIs($namespace_map, $alias_list_s)
243 foreach (explode(',', $alias_list_s) as $alias) {
244 $type_uri = $namespace_map->getNamespaceURI($alias);
245 if ($type_uri === null) {
247 // 'No type is defined for attribute name %r' % (alias,))
248 return new Auth_OpenID_AX_Error(
249 sprintf('No type is defined for attribute name %s',
262 * An attribute exchange 'fetch_request' message. This message is sent
263 * by a relying party when it wishes to obtain attributes about the
264 * subject of an OpenID authentication request.
268 class Auth_OpenID_AX_FetchRequest extends Auth_OpenID_AX_Message {
270 var $mode = 'fetch_request';
272 function Auth_OpenID_AX_FetchRequest($update_url=null)
275 * requested_attributes: The attributes that have been
276 * requested thus far, indexed by the type URI.
278 $this->requested_attributes = array();
281 * update_url: A URL that will accept responses for this
282 * attribute exchange request, even in the absence of the user
283 * who made this request.
285 $this->update_url = $update_url;
289 * Add an attribute to this attribute exchange request.
291 * @param attribute: The attribute that is being requested
292 * @return true on success, false when the requested attribute is
293 * already present in this fetch request.
295 function add($attribute)
297 if ($this->contains($attribute->type_uri)) {
298 return new Auth_OpenID_AX_Error(
299 sprintf("The attribute %s has already been requested",
300 $attribute->type_uri));
303 $this->requested_attributes[$attribute->type_uri] = $attribute;
309 * Get the serialized form of this attribute fetch request.
311 * @returns Auth_OpenID_AX_FetchRequest The fetch request message parameters
313 function getExtensionArgs()
315 $aliases = new Auth_OpenID_NamespaceMap();
318 $if_available = array();
320 $ax_args = $this->_newArgs();
322 foreach ($this->requested_attributes as $type_uri => $attribute) {
323 if ($attribute->alias === null) {
324 $alias = $aliases->add($type_uri);
326 $alias = $aliases->addAlias($type_uri, $attribute->alias);
328 if ($alias === null) {
329 return new Auth_OpenID_AX_Error(
330 sprintf("Could not add alias %s for URI %s",
331 $attribute->alias, $type_uri
336 if ($attribute->required) {
337 $required[] = $alias;
339 $if_available[] = $alias;
342 if ($attribute->count != 1) {
343 $ax_args['count.' . $alias] = strval($attribute->count);
346 $ax_args['type.' . $alias] = $type_uri;
350 $ax_args['required'] = implode(',', $required);
354 $ax_args['if_available'] = implode(',', $if_available);
361 * Get the type URIs for all attributes that have been marked as
364 * @return A list of the type URIs for attributes that have been
365 * marked as required.
367 function getRequiredAttrs()
370 foreach ($this->requested_attributes as $type_uri => $attribute) {
371 if ($attribute->required) {
372 $required[] = $type_uri;
380 * Extract a FetchRequest from an OpenID message
382 * @param request: The OpenID request containing the attribute
385 * @returns mixed An Auth_OpenID_AX_Error or the
386 * Auth_OpenID_AX_FetchRequest extracted from the request message if
389 static function fromOpenIDRequest($request)
391 $m = $request->message;
392 $obj = new Auth_OpenID_AX_FetchRequest();
393 $ax_args = $m->getArgs($obj->ns_uri);
395 $result = $obj->parseExtensionArgs($ax_args);
397 if (Auth_OpenID_AX::isError($result)) {
401 if ($obj->update_url) {
402 // Update URL must match the openid.realm of the
403 // underlying OpenID 2 message.
404 $realm = $m->getArg(Auth_OpenID_OPENID_NS, 'realm',
406 Auth_OpenID_OPENID_NS,
410 $obj = new Auth_OpenID_AX_Error(
411 sprintf("Cannot validate update_url %s " .
412 "against absent realm", $obj->update_url));
413 } else if (!Auth_OpenID_TrustRoot::match($realm,
415 $obj = new Auth_OpenID_AX_Error(
416 sprintf("Update URL %s failed validation against realm %s",
417 $obj->update_url, $realm));
425 * Given attribute exchange arguments, populate this FetchRequest.
427 * @return $result Auth_OpenID_AX_Error if the data to be parsed
428 * does not follow the attribute exchange specification. At least
429 * when 'if_available' or 'required' is not specified for a
430 * particular attribute type. Returns true otherwise.
432 function parseExtensionArgs($ax_args)
434 $result = $this->_checkMode($ax_args);
435 if (Auth_OpenID_AX::isError($result)) {
439 $aliases = new Auth_OpenID_NamespaceMap();
441 foreach ($ax_args as $key => $value) {
442 if (strpos($key, 'type.') === 0) {
443 $alias = substr($key, 5);
446 $alias = $aliases->addAlias($type_uri, $alias);
448 if ($alias === null) {
449 return new Auth_OpenID_AX_Error(
450 sprintf("Could not add alias %s for URI %s",
455 $count_s = Auth_OpenID::arrayGet($ax_args, 'count.' . $alias);
457 $count = Auth_OpenID::intval($count_s);
458 if (($count === false) &&
459 ($count_s === Auth_OpenID_AX_UNLIMITED_VALUES)) {
466 if ($count === false) {
467 return new Auth_OpenID_AX_Error(
468 sprintf("Integer value expected for %s, got %s",
469 'count.' . $alias, $count_s));
472 $attrinfo = Auth_OpenID_AX_AttrInfo::make($type_uri, $count,
475 if (Auth_OpenID_AX::isError($attrinfo)) {
479 $this->add($attrinfo);
483 $required = Auth_OpenID_AX_toTypeURIs($aliases,
484 Auth_OpenID::arrayGet($ax_args, 'required'));
486 foreach ($required as $type_uri) {
487 $attrib = $this->requested_attributes[$type_uri];
488 $attrib->required = true;
491 $if_available = Auth_OpenID_AX_toTypeURIs($aliases,
492 Auth_OpenID::arrayGet($ax_args, 'if_available'));
494 $all_type_uris = array_merge($required, $if_available);
496 foreach ($aliases->iterNamespaceURIs() as $type_uri) {
497 if (!in_array($type_uri, $all_type_uris)) {
498 return new Auth_OpenID_AX_Error(
499 sprintf('Type URI %s was in the request but not ' .
500 'present in "required" or "if_available"',
506 $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
512 * Iterate over the AttrInfo objects that are contained in this
517 return array_values($this->requested_attributes);
522 return array_keys($this->requested_attributes);
526 * Is the given type URI present in this fetch_request?
528 function contains($type_uri)
530 return in_array($type_uri, $this->iterTypes());
535 * An abstract class that implements a message that has attribute keys
536 * and values. It contains the common code between fetch_response and
541 class Auth_OpenID_AX_KeyValueMessage extends Auth_OpenID_AX_Message {
543 function Auth_OpenID_AX_KeyValueMessage()
545 $this->data = array();
549 * Add a single value for the given attribute type to the
550 * message. If there are already values specified for this type,
551 * this value will be sent in addition to the values already
554 * @param type_uri: The URI for the attribute
555 * @param value: The value to add to the response to the relying
556 * party for this attribute
559 function addValue($type_uri, $value)
561 if (!array_key_exists($type_uri, $this->data)) {
562 $this->data[$type_uri] = array();
565 $values =& $this->data[$type_uri];
570 * Set the values for the given attribute type. This replaces any
571 * values that have already been set for this attribute.
573 * @param type_uri: The URI for the attribute
574 * @param values: A list of values to send for this attribute.
576 function setValues($type_uri, &$values)
578 $this->data[$type_uri] =& $values;
582 * Get the extension arguments for the key/value pairs contained
585 * @param aliases: An alias mapping. Set to None if you don't care
586 * about the aliases for this request.
590 function _getExtensionKVArgs($aliases)
592 if ($aliases === null) {
593 $aliases = new Auth_OpenID_NamespaceMap();
598 foreach ($this->data as $type_uri => $values) {
599 $alias = $aliases->add($type_uri);
601 $ax_args['type.' . $alias] = $type_uri;
602 $ax_args['count.' . $alias] = strval(count($values));
604 foreach ($values as $i => $value) {
605 $key = sprintf('value.%s.%d', $alias, $i + 1);
606 $ax_args[$key] = $value;
614 * Parse attribute exchange key/value arguments into this object.
616 * @param ax_args: The attribute exchange fetch_response
617 * arguments, with namespacing removed.
619 * @return Auth_OpenID_AX_Error or true
621 function parseExtensionArgs($ax_args)
623 $result = $this->_checkMode($ax_args);
624 if (Auth_OpenID_AX::isError($result)) {
628 $aliases = new Auth_OpenID_NamespaceMap();
630 foreach ($ax_args as $key => $value) {
631 if (strpos($key, 'type.') === 0) {
633 $alias = substr($key, 5);
635 $result = Auth_OpenID_AX_checkAlias($alias);
637 if (Auth_OpenID_AX::isError($result)) {
641 $alias = $aliases->addAlias($type_uri, $alias);
643 if ($alias === null) {
644 return new Auth_OpenID_AX_Error(
645 sprintf("Could not add alias %s for URI %s",
652 foreach ($aliases->iteritems() as $pair) {
653 list($type_uri, $alias) = $pair;
655 if (array_key_exists('count.' . $alias, $ax_args) && ($ax_args['count.' . $alias] !== Auth_OpenID_AX_UNLIMITED_VALUES)) {
657 $count_key = 'count.' . $alias;
658 $count_s = $ax_args[$count_key];
660 $count = Auth_OpenID::intval($count_s);
662 if ($count === false) {
663 return new Auth_OpenID_AX_Error(
664 sprintf("Integer value expected for %s, got %s",
665 'count. %s' . $alias, $count_s,
666 Auth_OpenID_AX_UNLIMITED_VALUES)
671 for ($i = 1; $i < $count + 1; $i++) {
672 $value_key = sprintf('value.%s.%d', $alias, $i);
674 if (!array_key_exists($value_key, $ax_args)) {
675 return new Auth_OpenID_AX_Error(
677 "No value found for key %s",
681 $value = $ax_args[$value_key];
685 $key = 'value.' . $alias;
687 if (!array_key_exists($key, $ax_args)) {
688 return new Auth_OpenID_AX_Error(
690 "No value found for key %s",
694 $value = $ax_args['value.' . $alias];
699 $values = array($value);
703 $this->data[$type_uri] = $values;
710 * Get a single value for an attribute. If no value was sent for
711 * this attribute, use the supplied default. If there is more than
712 * one value for this attribute, this method will fail.
714 * @param type_uri: The URI for the attribute
715 * @param default: The value to return if the attribute was not
716 * sent in the fetch_response.
718 * @return $value Auth_OpenID_AX_Error on failure or the value of
719 * the attribute in the fetch_response message, or the default
722 function getSingle($type_uri, $default=null)
724 $values = Auth_OpenID::arrayGet($this->data, $type_uri);
727 } else if (count($values) == 1) {
730 return new Auth_OpenID_AX_Error(
731 sprintf('More than one value present for %s',
738 * Get the list of values for this attribute in the
741 * XXX: what to do if the values are not present? default
742 * parameter? this is funny because it's always supposed to return
743 * a list, so the default may break that, though it's provided by
744 * the user's code, so it might be okay. If no default is
745 * supplied, should the return be None or []?
747 * @param type_uri: The URI of the attribute
749 * @return $values The list of values for this attribute in the
750 * response. May be an empty list. If the attribute was not sent
751 * in the response, returns Auth_OpenID_AX_Error.
753 function get($type_uri)
755 if (array_key_exists($type_uri, $this->data)) {
756 return $this->data[$type_uri];
758 return new Auth_OpenID_AX_Error(
759 sprintf("Type URI %s not found in response",
766 * Get the number of responses for a particular attribute in this
767 * fetch_response message.
769 * @param type_uri: The URI of the attribute
771 * @returns int The number of values sent for this attribute. If
772 * the attribute was not sent in the response, returns
773 * Auth_OpenID_AX_Error.
775 function count($type_uri)
777 if (array_key_exists($type_uri, $this->data)) {
778 return count($this->get($type_uri));
780 return new Auth_OpenID_AX_Error(
781 sprintf("Type URI %s not found in response",
789 * A fetch_response attribute exchange message.
793 class Auth_OpenID_AX_FetchResponse extends Auth_OpenID_AX_KeyValueMessage {
794 var $mode = 'fetch_response';
796 function Auth_OpenID_AX_FetchResponse($update_url=null)
798 $this->Auth_OpenID_AX_KeyValueMessage();
799 $this->update_url = $update_url;
803 * Serialize this object into arguments in the attribute exchange
806 * @return $args The dictionary of unqualified attribute exchange
807 * arguments that represent this fetch_response, or
808 * Auth_OpenID_AX_Error on error.
810 function getExtensionArgs($request=null)
812 $aliases = new Auth_OpenID_NamespaceMap();
814 $zero_value_types = array();
816 if ($request !== null) {
817 // Validate the data in the context of the request (the
818 // same attributes should be present in each, and the
819 // counts in the response must be no more than the counts
822 foreach ($this->data as $type_uri => $unused) {
823 if (!$request->contains($type_uri)) {
824 return new Auth_OpenID_AX_Error(
825 sprintf("Response attribute not present in request: %s",
831 foreach ($request->iterAttrs() as $attr_info) {
832 // Copy the aliases from the request so that reading
833 // the response in light of the request is easier
834 if ($attr_info->alias === null) {
835 $aliases->add($attr_info->type_uri);
837 $alias = $aliases->addAlias($attr_info->type_uri,
840 if ($alias === null) {
841 return new Auth_OpenID_AX_Error(
842 sprintf("Could not add alias %s for URI %s",
843 $attr_info->alias, $attr_info->type_uri)
848 if (array_key_exists($attr_info->type_uri, $this->data)) {
849 $values = $this->data[$attr_info->type_uri];
852 $zero_value_types[] = $attr_info;
855 if (($attr_info->count != Auth_OpenID_AX_UNLIMITED_VALUES) &&
856 ($attr_info->count < count($values))) {
857 return new Auth_OpenID_AX_Error(
858 sprintf("More than the number of requested values " .
859 "were specified for %s",
860 $attr_info->type_uri)
866 $kv_args = $this->_getExtensionKVArgs($aliases);
868 // Add the KV args into the response with the args that are
869 // unique to the fetch_response
870 $ax_args = $this->_newArgs();
872 // For each requested attribute, put its type/alias and count
873 // into the response even if no data were returned.
874 foreach ($zero_value_types as $attr_info) {
875 $alias = $aliases->getAlias($attr_info->type_uri);
876 $kv_args['type.' . $alias] = $attr_info->type_uri;
877 $kv_args['count.' . $alias] = '0';
882 $update_url = $request->update_url;
884 $update_url = $this->update_url;
888 $ax_args['update_url'] = $update_url;
891 Auth_OpenID::update($ax_args, $kv_args);
897 * @return $result Auth_OpenID_AX_Error on failure or true on
900 function parseExtensionArgs($ax_args)
902 $result = parent::parseExtensionArgs($ax_args);
904 if (Auth_OpenID_AX::isError($result)) {
908 $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
914 * Construct a FetchResponse object from an OpenID library
915 * SuccessResponse object.
917 * @param success_response: A successful id_res response object
919 * @param signed: Whether non-signed args should be processsed. If
920 * True (the default), only signed arguments will be processsed.
922 * @return $response A FetchResponse containing the data from the
925 static function fromSuccessResponse($success_response, $signed=true)
927 $obj = new Auth_OpenID_AX_FetchResponse();
929 $ax_args = $success_response->getSignedNS($obj->ns_uri);
931 $ax_args = $success_response->message->getArgs($obj->ns_uri);
933 if ($ax_args === null || Auth_OpenID::isFailure($ax_args) ||
934 sizeof($ax_args) == 0) {
938 $result = $obj->parseExtensionArgs($ax_args);
939 if (Auth_OpenID_AX::isError($result)) {
948 * A store request attribute exchange message representation.
952 class Auth_OpenID_AX_StoreRequest extends Auth_OpenID_AX_KeyValueMessage {
953 var $mode = 'store_request';
956 * @param array $aliases The namespace aliases to use when making
957 * this store response. Leave as None to use defaults.
959 function getExtensionArgs($aliases=null)
961 $ax_args = $this->_newArgs();
962 $kv_args = $this->_getExtensionKVArgs($aliases);
963 Auth_OpenID::update($ax_args, $kv_args);
969 * An indication that the store request was processed along with this
970 * OpenID transaction. Use make(), NOT the constructor, to create
975 class Auth_OpenID_AX_StoreResponse extends Auth_OpenID_AX_Message {
976 var $SUCCESS_MODE = 'store_response_success';
977 var $FAILURE_MODE = 'store_response_failure';
980 * Returns Auth_OpenID_AX_Error on error or an
981 * Auth_OpenID_AX_StoreResponse object on success.
983 function make($succeeded=true, $error_message=null)
985 if (($succeeded) && ($error_message !== null)) {
986 return new Auth_OpenID_AX_Error('An error message may only be '.
987 'included in a failing fetch response');
990 return new Auth_OpenID_AX_StoreResponse($succeeded, $error_message);
993 function Auth_OpenID_AX_StoreResponse($succeeded=true, $error_message=null)
996 $this->mode = $this->SUCCESS_MODE;
998 $this->mode = $this->FAILURE_MODE;
1001 $this->error_message = $error_message;
1005 * Was this response a success response?
1007 function succeeded()
1009 return $this->mode == $this->SUCCESS_MODE;
1012 function getExtensionArgs()
1014 $ax_args = $this->_newArgs();
1015 if ((!$this->succeeded()) && $this->error_message) {
1016 $ax_args['error'] = $this->error_message;