diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2008-04-17 21:01:43 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2008-04-17 21:01:43 -0700 |
commit | 0fa78ed519bd570d01bea13f6c19d12fa5ab369f (patch) | |
tree | 7cb77a1929020f35d280664488e4f70c9fbc4acd | |
parent | 07b8b586c6873b88df126b4bd9947d87afe8f929 (diff) | |
download | DotNetOpenAuth-0fa78ed519bd570d01bea13f6c19d12fa5ab369f.zip DotNetOpenAuth-0fa78ed519bd570d01bea13f6c19d12fa5ab369f.tar.gz DotNetOpenAuth-0fa78ed519bd570d01bea13f6c19d12fa5ab369f.tar.bz2 |
Attribute Exchange extension is now feature complete.
Also added some unit and round-tripping tests.
-rw-r--r-- | doc/openid-attribute-exchange-1_0.html | 1157 | ||||
-rw-r--r-- | src/DotNetOpenId.Test/DotNetOpenId.Test.csproj | 2 | ||||
-rw-r--r-- | src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchRequestTests.cs | 73 | ||||
-rw-r--r-- | src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchResponseTests.cs | 52 | ||||
-rw-r--r-- | src/DotNetOpenId.Test/Extensions/AttributeExchangeTests.cs | 30 | ||||
-rw-r--r-- | src/DotNetOpenId.Test/Extensions/ExtensionTestBase.cs | 8 | ||||
-rw-r--r-- | src/DotNetOpenId.TestWeb/ProviderEndpoint.aspx.cs | 50 | ||||
-rw-r--r-- | src/DotNetOpenId/DotNetOpenId.csproj | 4 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/AliasManager.cs | 65 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/AttributeExchangeFetchRequest.cs | 109 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/AttributeExchangeFetchResponse.cs | 113 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/AttributeRequest.cs | 45 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/AttributeResponse.cs | 23 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/DemandLevel.cs | 21 | ||||
-rw-r--r-- | src/DotNetOpenId/Strings.Designer.cs | 18 | ||||
-rw-r--r-- | src/DotNetOpenId/Strings.resx | 6 |
16 files changed, 1760 insertions, 16 deletions
diff --git a/doc/openid-attribute-exchange-1_0.html b/doc/openid-attribute-exchange-1_0.html new file mode 100644 index 0000000..68babb4 --- /dev/null +++ b/doc/openid-attribute-exchange-1_0.html @@ -0,0 +1,1157 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"><head><title>Final: OpenID Attribute Exchange 1.0 - Final</title>
+
+<meta http-equiv="Expires" content="Wed, 05 Dec 2007 17:48:00 +0000">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="description" content="OpenID Attribute Exchange 1.0 - Final">
+<meta name="generator" content="xml2rfc v1.33pre5 (http://xml.resource.org/)">
+<style type="text/css"><!--
+ body {
+ font-family: verdana, charcoal, helvetica, arial, sans-serif;
+ font-size: small; color: #000; background-color: #FFF;
+ margin: 2em;
+ }
+ h1, h2, h3, h4, h5, h6 {
+ font-family: helvetica, monaco, "MS Sans Serif", arial, sans-serif;
+ font-weight: bold; font-style: normal;
+ }
+ h1 { color: #900; background-color: transparent; text-align: right; }
+ h3 { color: #333; background-color: transparent; }
+
+ td.RFCbug {
+ font-size: x-small; text-decoration: none;
+ width: 30px; height: 30px; padding-top: 2px;
+ text-align: justify; vertical-align: middle;
+ background-color: #000;
+ }
+ td.RFCbug span.RFC {
+ font-family: monaco, charcoal, geneva, "MS Sans Serif", helvetica, verdana, sans-serif;
+ font-weight: bold; color: #666;
+ }
+ td.RFCbug span.hotText {
+ font-family: charcoal, monaco, geneva, "MS Sans Serif", helvetica, verdana, sans-serif;
+ font-weight: normal; text-align: center; color: #FFF;
+ }
+
+ table.TOCbug { width: 30px; height: 15px; }
+ td.TOCbug {
+ text-align: center; width: 30px; height: 15px;
+ color: #FFF; background-color: #900;
+ }
+ td.TOCbug a {
+ font-family: monaco, charcoal, geneva, "MS Sans Serif", helvetica, sans-serif;
+ font-weight: bold; font-size: x-small; text-decoration: none;
+ color: #FFF; background-color: transparent;
+ }
+
+ td.header {
+ font-family: arial, helvetica, sans-serif; font-size: x-small;
+ vertical-align: top; width: 33%;
+ color: #FFF; background-color: #666;
+ }
+ td.author { font-weight: bold; font-size: x-small; margin-left: 4em; }
+ td.author-text { font-size: x-small; }
+
+ /* info code from SantaKlauss at http://www.madaboutstyle.com/tooltip2.html */
+ a.info {
+ /* This is the key. */
+ position: relative;
+ z-index: 24;
+ text-decoration: none;
+ }
+ a.info:hover {
+ z-index: 25;
+ color: #FFF; background-color: #900;
+ }
+ a.info span { display: none; }
+ a.info:hover span.info {
+ /* The span will display just on :hover state. */
+ display: block;
+ position: absolute;
+ font-size: smaller;
+ top: 2em; left: -5em; width: 15em;
+ padding: 2px; border: 1px solid #333;
+ color: #900; background-color: #EEE;
+ text-align: left;
+ }
+
+ a { font-weight: bold; }
+ a:link { color: #900; background-color: transparent; }
+ a:visited { color: #633; background-color: transparent; }
+ a:active { color: #633; background-color: transparent; }
+
+ p { margin-left: 2em; margin-right: 2em; }
+ p.copyright { font-size: x-small; }
+ p.toc { font-size: small; font-weight: bold; margin-left: 3em; }
+ table.toc { margin: 0 0 0 3em; padding: 0; border: 0; vertical-align: text-top; }
+ td.toc { font-size: small; font-weight: bold; vertical-align: text-top; }
+
+ ol.text { margin-left: 2em; margin-right: 2em; }
+ ul.text { margin-left: 2em; margin-right: 2em; }
+ li { margin-left: 3em; }
+
+ /* RFC-2629 <spanx>s and <artwork>s. */
+ em { font-style: italic; }
+ strong { font-weight: bold; }
+ dfn { font-weight: bold; font-style: normal; }
+ cite { font-weight: normal; font-style: normal; }
+ tt { color: #036; }
+ tt, pre, pre dfn, pre em, pre cite, pre span {
+ font-family: "Courier New", Courier, monospace; font-size: small;
+ }
+ pre {
+ text-align: left; padding: 4px;
+ color: #000; background-color: #CCC;
+ }
+ pre dfn { color: #900; }
+ pre em { color: #66F; background-color: #FFC; font-weight: normal; }
+ pre .key { color: #33C; font-weight: bold; }
+ pre .id { color: #900; }
+ pre .str { color: #000; background-color: #CFF; }
+ pre .val { color: #066; }
+ pre .rep { color: #909; }
+ pre .oth { color: #000; background-color: #FCF; }
+ pre .err { background-color: #FCC; }
+
+ /* RFC-2629 <texttable>s. */
+ table.all, table.full, table.headers, table.none {
+ font-size: small; text-align: center; border-width: 2px;
+ vertical-align: top; border-collapse: collapse;
+ }
+ table.all, table.full { border-style: solid; border-color: black; }
+ table.headers, table.none { border-style: none; }
+ th {
+ font-weight: bold; border-color: black;
+ border-width: 2px 2px 3px 2px;
+ }
+ table.all th, table.full th { border-style: solid; }
+ table.headers th { border-style: none none solid none; }
+ table.none th { border-style: none; }
+ table.all td {
+ border-style: solid; border-color: #333;
+ border-width: 1px 2px;
+ }
+ table.full td, table.headers td, table.none td { border-style: none; }
+
+ hr { height: 1px; }
+ hr.insert {
+ width: 80%; border-style: none; border-width: 0;
+ color: #CCC; background-color: #CCC;
+ }
+--></style></head><body>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<table summary="layout" border="0" cellpadding="0" cellspacing="0" width="66%"><tbody><tr><td><table summary="layout" border="0" cellpadding="2" cellspacing="1" width="100%">
+<tbody><tr><td class="header">Final</td><td class="header">D. Hardt</td></tr>
+<tr><td class="header"> </td><td class="header">J. Bufu</td></tr>
+<tr><td class="header"> </td><td class="header">Sxip Identity</td></tr>
+<tr><td class="header"> </td><td class="header">J. Hoyt</td></tr>
+<tr><td class="header"> </td><td class="header">JanRain</td></tr>
+<tr><td class="header"> </td><td class="header">December 5, 2007</td></tr>
+</tbody></table></td></tr></tbody></table>
+<h1><br>OpenID Attribute Exchange 1.0 - Final</h1>
+
+<h3>Abstract</h3>
+
+<p>
+ OpenID Attribute Exchange is an OpenID service extension for
+ exchanging identity information between endpoints. Messages for
+ retrieval and storage of identity information are provided.
+
+</p><a name="toc"></a><br><hr>
+<h3>Table of Contents</h3>
+<p class="toc">
+<a href="#anchor1">1.</a>
+Terminology<br>
+ <a href="#anchor2">1.1.</a>
+Definitions and Conventions<br>
+<a href="#anchor3">2.</a>
+Overview<br>
+<a href="#anchor4">3.</a>
+Information Model<br>
+ <a href="#identifier-definition">3.1.</a>
+Subject Identifier<br>
+ <a href="#attribute-name-definition">3.2.</a>
+Attribute Type Identifier<br>
+ <a href="#attribute-value-definition">3.3.</a>
+Attribute Value<br>
+ <a href="#attribute-specific-encodings">3.3.1.</a>
+Attribute-Specific Encodings<br>
+<a href="#anchor5">4.</a>
+Discovery<br>
+<a href="#fetch">5.</a>
+Fetch Message<br>
+ <a href="#fetch_request">5.1.</a>
+Fetch Request Format<br>
+ <a href="#fetch_response">5.2.</a>
+Fetch Response Format<br>
+<a href="#store">6.</a>
+Store Message<br>
+ <a href="#store_request">6.1.</a>
+Store Request Format<br>
+ <a href="#store_response">6.2.</a>
+Store Response Format<br>
+ <a href="#anchor6">6.2.1.</a>
+Storage Success<br>
+ <a href="#anchor7">6.2.2.</a>
+Storage Failure<br>
+<a href="#anchor8">7.</a>
+Security Considerations<br>
+<a href="#anchor9">8.</a>
+Acknowledgements<br>
+<a href="#rfc.references1">9.</a>
+References<br>
+ <a href="#rfc.references1">9.1.</a>
+Normative References<br>
+ <a href="#rfc.references2">9.2.</a>
+Non-normative References<br>
+<a href="#rfc.authors">§</a>
+Authors' Addresses<br>
+</p>
+<br clear="all">
+
+<a name="anchor1"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.1"></a><h3>1.
+Terminology</h3>
+
+<p>
+ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
+ NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+ "OPTIONAL" in this document are to be interpreted as described
+ in <a class="info" href="#RFC2119">[RFC2119]<span> (</span><span class="info">Bradner, S., “Key words for use in RFCs to Indicate Requirement Levels,” March 1997.</span><span>)</span></a>.
+
+</p>
+<a name="anchor2"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.1.1"></a><h3>1.1.
+Definitions and Conventions</h3>
+
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>User:</dt>
+<dd>
+ Also referred to as "End User" or "Subject".
+ A person with a digital identity who participates in
+ OpenID-based identity information exchanges using their
+ client software, typically a web browser.
+
+</dd>
+<dt>Identity Data:</dt>
+<dd>
+ A property of a digital identity in which the Property
+ Name and Property Value are represented as a name-value
+ pair.
+
+</dd>
+<dt>Attribute</dt>
+<dd>
+ The base of the information model used to describe the
+ Identity Data, for the purpose of exchanging it.
+
+</dd>
+<dt>Persona:</dt>
+<dd>
+ A subset of the user's identity data. A user can have
+ multiple personas as part of their identity. For example,
+ a user might have a work persona and a home persona.
+
+</dd>
+<dt>OpenID Provider:</dt>
+<dd>
+ Also called "OP" or "Server". An OpenID Authentication
+ server on which a Relying Party relies for an assertion
+ that the end user controls an Identifier.
+
+</dd>
+<dt>Relying Party:</dt>
+<dd>
+ Also called "RP" or "Consumer". A Web application that
+ wants proof that the end user controls an Identifier,
+ and requests identity data associated with the end user.
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+ All OpenID Attribute Exchange messages MUST contain the
+ following extension namespace declaration, as specified
+ in the Extensions section of OpenID-Authentication-2.0:
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.<extension_alias>=http://openid.net/srv/ax/1.0
+
+</pre></div>
+<p>
+ The actual extension namespace alias should be determined
+ on a per-message basis by the party composing the messages,
+ in such a manner as to avoid conflicts between multiple
+ extensions. For the purposes of this document, the extension
+ namespace alias for the attribute exchange service will be "ax".
+
+</p>
+<a name="anchor3"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.2"></a><h3>2.
+Overview</h3>
+
+<p>
+ The attribute exchange service extension is identified by the
+ URI "http://openid.net/srv/ax/1.0". This URI MUST be specified in the extension
+ namespace declaration.
+
+</p>
+<p>
+ An attribute is a unit of personal identity information that
+ is identified by a unique URI. It may refer to any kind of
+ information. A reference example of defining attribute types
+ is provided by <a class="info" href="#OpenID.axschema">[OpenID.axschema]<span> (</span><span class="info">Hardt, D., “Schema for OpenID Attribute Exchange,” May 2007.</span><span>)</span></a>.
+
+</p>
+<p>
+ This service extension defines two message types for
+ transferring attributes: fetch (see <a class="info" href="#fetch">Section 5<span> (</span><span class="info">Fetch Message</span><span>)</span></a>)
+ and store (see <a class="info" href="#store">Section 6<span> (</span><span class="info">Store Message</span><span>)</span></a>). Fetch retrieves
+ attribute information from an OpenID Provider, while store
+ saves or updates attribute information on the OpenID
+ Provider. Both messages originate from the Relying Party
+ and are passed to the OpenID Provider via the user agent
+ as per the OpenID Authentication protocol specification.
+
+</p>
+<p>
+ The request parameters detailed here MUST be sent using the
+ <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a> extension mechanism.
+
+</p>
+<a name="anchor4"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.3"></a><h3>3.
+Information Model</h3>
+
+<p>
+ The OpenID Attribute Exchange service extension provides a
+ mechanism for moving identity information between sites, as
+ such its information model is simple:
+ </p>
+<blockquote class="text">
+<p>An attribute is associated with a Subject Identifier
+</p>
+<p>An attribute has a type identifier and a value
+</p>
+<p>An attribute type identifier is a URI
+</p>
+<p>An attribute value can be any kind of data.
+</p>
+</blockquote><p>
+
+</p>
+<a name="identifier-definition"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.3.1"></a><h3>3.1.
+Subject Identifier</h3>
+
+<p>
+ An identifier for a set of attributes. It MUST be a URI. The
+ subject identifier corresponds to the end-user identifier in
+ the authentication portion of the messages. In other words,
+ the subject of the identity attributes in the attribute
+ exchange part of the message is the same as the end-user in
+ the authentication part. The subject identifier is not
+ included in the attribute exchange.
+
+</p>
+<a name="attribute-name-definition"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.3.2"></a><h3>3.2.
+Attribute Type Identifier</h3>
+
+<p>
+ An attribute type identifier MUST be a URI, which is used
+ for referring to property values.
+
+</p>
+<p>
+ If an attribute type identifier URI can be resolved then it
+ MAY be dereferenced to retrieve a description of the
+ property. OpenID Providers can use the metadata obtained
+ through dereferencing new or unknown attribute types to
+ dynamically assist the user in providing the attribute.
+
+</p>
+<p>
+ This provides for flexibility and extensibility. Flexibility
+ in that both URNs and URLs can be used to refer to property
+ values. Extensibility allows any individual site, or
+ consortium of sites, to define their own attribute types
+ with agreements on the syntax and semantics of their
+ associated attribute values.
+
+</p>
+<p>
+ <a class="info" href="#OpenID.axschema">[OpenID.axschema]<span> (</span><span class="info">Hardt, D., “Schema for OpenID Attribute Exchange,” May 2007.</span><span>)</span></a> outlines an example method
+ of defining new attribute type URIs, and also provides a set
+ of attribute types with their associated metadata schema and
+ data formats.
+
+</p>
+<a name="attribute-value-definition"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.3.3"></a><h3>3.3.
+Attribute Value</h3>
+
+<p>
+ A attribute value MUST be a <a class="info" href="#RFC3629">UTF-8<span> (</span><span class="info">Yergeau, F., “UTF-8, a transformation format of ISO 10646,” November 2003.</span><span>)</span></a> [RFC3629]
+ string. In order to comply with the data formats defined by
+ the underlying <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>
+ protocol, attribute values MUST NOT contain newlines
+ (UCS codepoint 10, "\n").
+
+</p>
+<p>
+ OpenID Attribute Exchange can be used to transfer any kind
+ of data. If the data contains newlines, is not a UTF-8 string
+ or it is so desired by the parties transferring the data,
+ the data MUST be encoded to a UTF-8 string without newlines.
+
+</p>
+<a name="attribute-specific-encodings"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.3.3.1"></a><h3>3.3.1.
+Attribute-Specific Encodings</h3>
+
+<p>
+ Attribute-specific encodings can be defined using the
+ attribute metadata descriptions and may be applied by
+ the protocol layer above OpenID Attribute Exchange.
+
+</p>
+<p>
+ Optionally, attribute-specific encodings may use language
+ tags <a class="info" href="#OpenID.value-lang-1.0">[OpenID.value‑lang‑1.0]<span> (</span><span class="info">Wahl, M., “Language Tags for OpenID Values,” April 2007.</span><span>)</span></a> for
+ localization.
+
+</p>
+<a name="anchor5"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.4"></a><h3>4.
+Discovery</h3>
+
+<p>
+ Discovery of the attribute exchange service extension is
+ achieved via the mechanism described in <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>. The attribute exchange
+ namespace "http://openid.net/srv/ax/1.0" SHOULD be listed as an <xrd:Type>
+ child element of the <xrd:Service> element in the XRDS
+ discovery document.
+
+</p>
+<a name="fetch"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.5"></a><h3>5.
+Fetch Message</h3>
+
+<p>
+ The fetch message is used to retrieve personal identity
+ attributes from an OpenID Provider.
+
+</p>
+<a name="fetch_request"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.5.1"></a><h3>5.1.
+Fetch Request Format</h3>
+
+<p>
+ With the exception of "openid.ax.mode", all of the following
+ request fields are OPTIONAL, though at least one of
+ "openid.ax.required" or "openid.ax.if_available" MUST be
+ specified in the request, and any attribute alias present in a
+ "openid.ax.required" or "openid.ax.if_available" parameter MUST
+ have an associated "openid.ax.type.<alias>" parameter.
+ The supported length for attribute aliases MUST be at least
+ 32 characters.
+
+</p>
+<p>
+ Multiple attribute aliases in the "openid.ax.required" and
+ "openid.ax.if_available" directives are separated with a
+ comma, ",".
+
+</p>
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>openid.ax.mode</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ REQUIRED. Value: "fetch_request".
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.type.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The value of this parameter specifies the type identifier
+ URI of a requested attribute. The <alias> will
+ further be used to identify the attribute being exchanged.
+
+</p>
+<p>
+ Attribute aliases MUST NOT contain newline and colon characters,
+ as specified in the Data Formats / Protocol Messages section of
+ <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>; they also MUST
+ NOT contain commas (",") and periods (".").
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.required</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Value: an attribute alias, or a list of aliases
+ corresponding to the URIs defined by
+ "openid.ax.type.<alias>" parameters. Multiple
+ attribute aliases are separated with a comma, ",".
+
+</p>
+<p>
+ By requesting attributes using this field, a hint is sent
+ to the OP about the RP's requirements for offering certain
+ functionality and should be used by the OP to help the
+ user decide what attributes to release. RP's requirements
+ should not be enforced by the OP.
+
+</p>
+<p>
+ The RP should offer, out of band of attribute exchange,
+ an alternate method of collecting the attributes it needs,
+ if they weren't obtained via attribute exchange.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.if_available</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Value: an attribute alias, or a list of aliases
+ corresponding to the URIs defined by
+ "openid.ax.type.<alias>" parameters. Multiple
+ attribute aliases are separated with a comma, ",".
+
+</p>
+<p>
+ Attributes requested using this field are deemed
+ optional by the RP; the RP should be able to complete
+ the interaction with the user even if values are not
+ provided by the OP for the optional attributes.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.count.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The number of values for the specified attribute
+ alias the Relying Party wishes to receive from the OpenID
+ Provider. If present, the value MUST be greater than zero,
+ or the special value "unlimited" which signifies that the
+ RP is requesting as many values as the OP has for the
+ attribute. If absent, exactly one value is requested.
+
+</p>
+<p>
+ OpenID Providers MAY return less than or the exact
+ number of values speficied by this field for the
+ associated attribute, but MUST NOT return more than
+ the number of requested values for the attribute.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.update_url</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ If present, the OpenID Provider may re-post the fetch
+ response message to the specified URL at some time
+ after the initial response has been sent, using a
+ OpenID Authentication Positive Assertion. If the
+ OpenID Provider supports this feature it MUST return
+ the parameter as part of the fetch response message.
+ If it does not support this feature it may legally
+ ignore this parameter.
+
+</p>
+<p>
+ The value of the "openid.ax.update_url" field MUST
+ be used as value for "openid.return_to" field of the
+ underlying OpenID Authentication Positive Assertion
+ of the fetch response update.
+
+</p>
+<p>
+ The "openid.ax.update_url" value MUST also match the
+ realm specified in the underlying OpenID message of the
+ fetch request, if a "openid.realm" field is present.
+ The matching rules are the ones specified in the
+ "Realms" section of the OpenID Authentication protocol.
+
+</p>
+<p>
+ This "unsolicited" response message would be
+ generated in response to an attribute information
+ update, and would contain the updated data. The OP
+ should obtain the user's consent for resending the
+ updated data to the RPs, as with any OpenID Positive
+ Assertion.
+
+</p>
+<p>
+ The relying party may include transaction data encoded
+ in the URL such that it contains enough information to
+ match the attribute information to the identity subject.
+ Additional information may be encoded in the URL by the
+ relying party as necessary.
+
+</p>
+<p>
+ If an RP wishes to receive no further updates for an
+ attribute, it MAY return the HTTP 404 response code to
+ the corresponding "update_url". OPs MAY decide to
+ stop sending updates after encountering 404 response
+ codes.
+
+</p>
+</blockquote>
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+ This example requests the required full name and gender
+ information, and the optional favourite dog and movie
+ information. The Relying Party is interested in up to three
+ favorite movies associated with the subject identifier.
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.ax=http://openid.net/srv/ax/1.0
+openid.ax.mode=fetch_request
+openid.ax.type.fname=http://example.com/schema/fullname
+openid.ax.type.gender=http://example.com/schema/gender
+openid.ax.type.fav_dog=http://example.com/schema/favourite_dog
+openid.ax.type.fav_movie=http://example.com/schema/favourite_movie
+openid.ax.count.fav_movie=3
+openid.ax.required=fname,gender
+openid.ax.if_available=fav_dog,fav_movie
+openid.ax.update_url=http://idconsumer.com/update?transaction_id=a6b5c41
+
+</pre></div>
+<a name="fetch_response"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.5.2"></a><h3>5.2.
+Fetch Response Format</h3>
+
+<p>
+ The fetch response message supplies the information
+ requested in the fetch request. Each attribute is supplied
+ with the assigned alias prefixed by "openid.ax.value." as the
+ lvalue and the attribute value as the rvalue. Attribute
+ types are also returned in the "openid.ax.type.<alias>"
+ parameters. The supported length for attribute aliases MUST
+ be at least 32 characters.
+
+</p>
+<p>
+ With the exception of "openid.ax.mode", all of the following
+ request fields are OPTIONAL, though any attribute value
+ present in a "openid.ax.value.<alias>" parameter MUST
+ have an associated "openid.ax.type.<alias>" parameter.
+
+</p>
+<p>
+ If a value was not supplied or available from the user,
+ the associated "openid.ax.value.<alias>" field
+ SHOULD NOT be included by the OP in the fetch response.
+ An "openid.ax.count.<alias>" with a value of "0"
+ together with its corresponding "openid.ax.type.<alias>"
+ field MAY be included to explicitly state that no values
+ are provided for an attribute.
+
+</p>
+<p>
+ Validation of the received data should be performed out of band
+ of attribute exchange by the RP.
+
+</p>
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>openid.ax.mode</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ REQUIRED. Value: "fetch_response".
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.type.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The value of this parameter specifies the type identifier
+ URI for an attribute in the fetch response.
+ The <alias> will further be used to identify
+ the attribute being exchanged.
+
+</p>
+<p>
+ Attribute aliases MUST NOT contain newline and colon characters,
+ as specified in the Data Formats / Protocol Messages section of
+ <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>; they also MUST
+ NOT contain commas (",") and periods (".").
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.count.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The number of values returned for the attribute referred
+ to as <alias>.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.value.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Assigns a value to the attribute referred to as
+ <alias>. This parameter format MUST be used if
+ "openid.ax.count.<alias>" is not sent.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.value.<alias>.<number></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Assigns a value to the attribute referred to as
+ <alias>. This parameter format MUST be used
+ if "openid.ax.count.<alias>" is sent and at least
+ one value is provided for the associated attribute.
+
+</p>
+<p>
+ The <number> uniquely identifies the index of
+ the value, ranging from one to the value specified by
+ "openid.ax.count.<alias>". The number of
+ parameters MUST be equal to the value specified by
+ "openid.ax.count.<alias>". The OP is not
+ required to preserve the order of attribute values
+ among fetch responses.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.update_url</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Returns the "update_url" parameter specified in the
+ request. If the OpenID Provider receives an
+ "update_url" parameter and it intends to support the
+ attribute update feature, it MUST present the
+ "update_url" parameter and value as part of the fetch
+ response message.
+
+</p>
+</blockquote>
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+ A fetch response message may also be sent to the
+ "update_url" specified in <a class="info" href="#fetch_request">Section 5.1<span> (</span><span class="info">Fetch Request Format</span><span>)</span></a> in response to attribute value
+ updates on the OpenID Provider.
+
+</p>
+<p>
+ The response to the previous request example, in which the
+ required full name information, and the optional favourite
+ dog information are supplied. Even though three movie names
+ were requested, the OP supplied only two values.
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.ax=http://openid.net/srv/ax/1.0
+openid.ax.mode=fetch_response
+openid.ax.type.fname=http://example.com/schema/fullname
+openid.ax.type.gender=http://example.com/schema/gender
+openid.ax.type.fav_dog=http://example.com/schema/favourite_dog
+openid.ax.type.fav_movie=http://example.com/schema/favourite_movie
+openid.ax.value.fname=John Smith
+openid.ax.count.gender=0
+openid.ax.value.fav_dog=Spot
+openid.ax.count.fav_movie=2
+openid.ax.value.fav_movie.1=Movie1
+openid.ax.value.fav_movie.2=Movie2
+openid.ax.update_url=http://idconsumer.com/update?transaction_id=a6b5c41
+
+</pre></div>
+<a name="store"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.6"></a><h3>6.
+Store Message</h3>
+
+<p>
+ The store message is used to store personal identity
+ information to the OpenID Provider; it provides the means
+ for an RP to transfer to the OP attributes that the user
+ may consider useful, such as by providing them to other RPs.
+ The supported length for attribute aliases MUST be at least
+ 32 characters.
+
+</p>
+<p>
+ The manner in which the OP processes the attribute payload in a
+ store request if out of scope of this document.
+
+</p>
+<a name="store_request"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.6.1"></a><h3>6.1.
+Store Request Format</h3>
+
+<p>
+ With the exception of "openid.ax.mode", all of the following
+ request fields are OPTIONAL. Any alias referred to in a
+ "openid.ax.value.<alias>" or
+ "openid.ax.value.<alias>.<number>" parameter MUST
+ have an associated "openid.ax.type.<alias>" parameter.
+
+</p>
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>openid.ax.mode</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ REQUIRED. Value: "store_request".
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.type.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The value of this parameter specifies the type identifier
+ URI for an attribute in the sore request.
+ The <alias> will further be used to identify
+ the attribute being exchanged.
+
+</p>
+<p>
+ Attribute aliases MUST NOT contain newline and colon characters,
+ as specified in the Data Formats / Protocol Messages section of
+ <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>; they also MUST
+ NOT contain commas (",") and periods (".").
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.count.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ The number of values sent for the attribute referred to
+ as <alias>. If present, it MUST be greater than
+ zero.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.value.<alias></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Assigns a value to the attribute referred to as
+ <alias>. This parameter format MUST be used if
+ "openid.ax.count.<alias>" is not sent.
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.value.<alias>.<number></dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ Assigns a value to the attribute referred to as
+ <alias>. The <number> uniquely identifies the
+ index of the value, ranging from one to the value specified
+ by "openid.ax.count.<alias>". This parameter format
+ MUST be used if "openid.ax.count.<alias>" is sent,
+ and the number of these parameters MUST be equal to the
+ value specified by "openid.ax.count.<alias>".
+
+</p>
+</blockquote>
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.ax=http://openid.net/srv/ax/1.0
+openid.ax.mode=store_request
+openid.ax.type.fname=http://example.com/schema/fullname
+openid.ax.value.fname=Bob Smith
+openid.ax.type.fav_movie=http://example.com/schema/favourite_movie
+openid.ax.count.fav_movie=2
+openid.ax.value.fav_movie.1=Movie1
+openid.ax.value.fav_movie.2=Movie2
+
+</pre></div>
+<a name="store_response"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.6.2"></a><h3>6.2.
+Store Response Format</h3>
+
+<a name="anchor6"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.6.2.1"></a><h3>6.2.1.
+Storage Success</h3>
+
+<p>
+ The successful store operation is indicated by the mode
+ parameter in the store response:
+
+</p>
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>openid.ax.mode</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ REQUIRED. Value: "store_response_success".
+
+</p>
+</blockquote>
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.ax=http://openid.net/srv/ax/1.0
+openid.ax.mode=store_response_success
+
+</pre></div>
+<a name="anchor7"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.6.2.2"></a><h3>6.2.2.
+Storage Failure</h3>
+
+<p>
+ A storage failure response has the following format:
+
+</p>
+<p>
+ </p>
+<blockquote class="text"><dl>
+<dt>openid.ax.mode</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ REQUIRED. Value: "store_response_failure".
+
+</p>
+</blockquote>
+
+</dd>
+<dt>openid.ax.error</dt>
+<dd>
+
+<blockquote class="text">
+<p>
+ OPTIONAL. Parameter describing the error condition
+ leading to the failure response, intended to be
+ presented to the user. The locale of the message
+ should match the locale of the HTTP message.
+
+</p>
+</blockquote>
+
+</dd>
+</dl></blockquote><p>
+
+</p>
+<p>
+
+</p><div style="display: table; width: 0pt; margin-left: 3em; margin-right: auto;"><pre>
+openid.ns.ax=http://openid.net/srv/ax/1.0
+openid.ax.mode=store_response_failure
+openid.ax.error=General storage failure
+
+</pre></div>
+<a name="anchor8"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.7"></a><h3>7.
+Security Considerations</h3>
+
+<p>
+ OpenID Attribute Exchange is an OpenID extension, and thus uses
+ OpenID Authentication request and response messages for exchanging
+ attributes.
+
+</p>
+<p>
+ See the "Security Considerations" section of
+ <a class="info" href="#OpenID.authentication-2.0">[OpenID.authentication‑2.0]<span> (</span><span class="info">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007.</span><span>)</span></a>.
+
+</p>
+<a name="anchor9"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.8"></a><h3>8.
+Acknowledgements</h3>
+
+<p>
+ John Merrells and other contributors to the document
+ 'draft-merrells-dix'. Portions of that document were
+ re-used for this one.
+
+</p>
+<p>
+ Mark Wahl advised on how to deal with issues concerning the
+ encoding of attributes.
+
+</p>
+<a name="rfc.references"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<a name="rfc.section.9"></a><h3>9.
+References</h3>
+
+<a name="rfc.references1"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<h3>9.1. Normative References</h3>
+<table border="0" width="99%">
+<tbody><tr><td class="author-text" valign="top"><a name="OpenID.authentication-2.0">[OpenID.authentication-2.0]</a></td>
+<td class="author-text">specs@openid.net, “OpenID Authentication 2.0 - Final,” August 2007 (<a href="http://www.openid.net/specs/openid-authentication-2_0.txt">TXT</a>, <a href="http://www.openid.net/specs/openid-authentication-2_0.html">HTML</a>).</td></tr>
+<tr><td class="author-text" valign="top"><a name="OpenID.value-lang-1.0">[OpenID.value-lang-1.0]</a></td>
+<td class="author-text"><a href="mailto:mark.wahl@informed-control.com">Wahl, M.</a>, “<a href="http://www.ldap.com/1/spec/schema/openid-value-lang-1_0-00.html">Language Tags for OpenID Values</a>,” April 2007.</td></tr>
+<tr><td class="author-text" valign="top"><a name="RFC2119">[RFC2119]</a></td>
+<td class="author-text"><a href="mailto:sob@harvard.edu">Bradner, S.</a>, “<a href="http://tools.ietf.org/html/rfc2119">Key words for use in RFCs to Indicate Requirement Levels</a>,” BCP 14, RFC 2119, March 1997 (<a href="ftp://ftp.isi.edu/in-notes/rfc2119.txt">TXT</a>, <a href="http://xml.resource.org/public/rfc/html/rfc2119.html">HTML</a>, <a href="http://xml.resource.org/public/rfc/xml/rfc2119.xml">XML</a>).</td></tr>
+<tr><td class="author-text" valign="top"><a name="RFC3629">[RFC3629]</a></td>
+<td class="author-text">Yergeau, F., “<a href="http://tools.ietf.org/html/rfc3629">UTF-8, a transformation format of ISO 10646</a>,” STD 63, RFC 3629, November 2003 (<a href="ftp://ftp.isi.edu/in-notes/rfc3629.txt">TXT</a>).</td></tr>
+</tbody></table>
+
+<a name="rfc.references2"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<h3>9.2. Non-normative References</h3>
+<table border="0" width="99%">
+<tbody><tr><td class="author-text" valign="top"><a name="OpenID.axschema">[OpenID.axschema]</a></td>
+<td class="author-text"><a href="mailto:dick@sxip.com">Hardt, D.</a>, “<a href="http://www.axschema.org/">Schema for OpenID Attribute Exchange</a>,” May 2007.</td></tr>
+</tbody></table>
+
+<a name="rfc.authors"></a><br><hr>
+<table summary="layout" class="TOCbug" align="right" cellpadding="0" cellspacing="2"><tbody><tr><td class="TOCbug"><a href="#toc"> TOC </a></td></tr></tbody></table>
+<h3>Authors' Addresses</h3>
+<table border="0" cellpadding="0" cellspacing="0" width="99%">
+<tbody><tr><td class="author-text"> </td>
+<td class="author-text">Dick Hardt</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Sxip Identity</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">798 Beatty Street</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Vancouver, BC V6B 2M1</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">CA</td></tr>
+<tr><td class="author" align="right">Email: </td>
+<td class="author-text"><a href="mailto:dick@sxip.com">dick@sxip.com</a></td></tr>
+<tr><td class="author" align="right">URI: </td>
+<td class="author-text"><a href="http://sxip.com/">http://sxip.com/</a></td></tr>
+<tr cellpadding="3"><td> </td><td> </td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Johnny Bufu</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Sxip Identity</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">798 Beatty Street</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Vancouver, BC V6B 2M1</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">CA</td></tr>
+<tr><td class="author" align="right">Email: </td>
+<td class="author-text"><a href="mailto:johnny@sxip.com">johnny@sxip.com</a></td></tr>
+<tr><td class="author" align="right">URI: </td>
+<td class="author-text"><a href="http://sxip.com/">http://sxip.com/</a></td></tr>
+<tr cellpadding="3"><td> </td><td> </td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Josh Hoyt</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">JanRain</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">5331 SW Macadam Ave. #375</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">Portland, OR 97239</td></tr>
+<tr><td class="author-text"> </td>
+<td class="author-text">US</td></tr>
+<tr><td class="author" align="right">Email: </td>
+<td class="author-text"><a href="mailto:josh@janrain.com">josh@janrain.com</a></td></tr>
+<tr><td class="author" align="right">URI: </td>
+<td class="author-text"><a href="http://janrain.com/">http://janrain.com/</a></td></tr>
+</tbody></table>
+
+</body></html>
\ No newline at end of file diff --git a/src/DotNetOpenId.Test/DotNetOpenId.Test.csproj b/src/DotNetOpenId.Test/DotNetOpenId.Test.csproj index bc71e0e..cc7054c 100644 --- a/src/DotNetOpenId.Test/DotNetOpenId.Test.csproj +++ b/src/DotNetOpenId.Test/DotNetOpenId.Test.csproj @@ -44,6 +44,8 @@ <Compile Include="AssociationsTest.cs" />
<Compile Include="AssociationTestSuite.cs" />
<Compile Include="ExtensionsArgumentsManagerTests.cs" />
+ <Compile Include="Extensions\AttributeExchangeFetchRequestTests.cs" />
+ <Compile Include="Extensions\AttributeExchangeFetchResponseTests.cs" />
<Compile Include="Extensions\AttributeExchangeTests.cs" />
<Compile Include="Extensions\ExtensionTestBase.cs" />
<Compile Include="Extensions\SimpleRegistrationTests.cs" />
diff --git a/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchRequestTests.cs b/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchRequestTests.cs new file mode 100644 index 0000000..3095b02 --- /dev/null +++ b/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchRequestTests.cs @@ -0,0 +1,73 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using DotNetOpenId.Extensions;
+
+namespace DotNetOpenId.Test.Extensions {
+ [TestFixture]
+ public class AttributeExchangeFetchRequestTests {
+ [Test, ExpectedException(typeof(ArgumentNullException))]
+ public void AddAttributeRequestNull() {
+ new AttributeExchangeFetchRequest().AddAttribute(null);
+ }
+
+ [Test]
+ public void AddAttributeRequest() {
+ var req = new AttributeExchangeFetchRequest();
+ req.AddAttribute(new AttributeRequest() { TypeUri = "http://someUri" });
+ }
+
+ [Test]
+ public void AddAttributeRequestStrangeUri() {
+ var req = new AttributeExchangeFetchRequest();
+ req.AddAttribute(new AttributeRequest() { TypeUri = "=someUri*who*knows*but*this*is*legal" });
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void AddAttributeRequestAgain() {
+ var req = new AttributeExchangeFetchRequest();
+ req.AddAttribute(new AttributeRequest() { TypeUri = "http://UriTwice" });
+ req.AddAttribute(new AttributeRequest() { TypeUri = "http://UriTwice" });
+ }
+
+ [Test]
+ public void RespondSimpleValue() {
+ var req = new AttributeRequest();
+ req.TypeUri = "http://someType";
+ var resp = req.Respond("value");
+ Assert.AreEqual(req.TypeUri, resp.TypeUri);
+ Assert.AreEqual(1, resp.Values.Length);
+ Assert.AreEqual("value", resp.Values[0]);
+ }
+
+ [Test]
+ public void RespondTwoValues() {
+ var req = new AttributeRequest();
+ req.TypeUri = "http://someType";
+ req.Count = 2;
+ var resp = req.Respond("value1", "value2");
+ Assert.AreEqual(req.TypeUri, resp.TypeUri);
+ Assert.AreEqual(2, resp.Values.Length);
+ Assert.AreEqual("value1", resp.Values[0]);
+ Assert.AreEqual("value2", resp.Values[1]);
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void RespondTooManyValues() {
+ var req = new AttributeRequest();
+ req.TypeUri = "http://someType";
+ req.Count = 1;
+ req.Respond("value1", "value2");
+ }
+
+ [Test, ExpectedException(typeof(ArgumentNullException))]
+ public void RespondNull() {
+ var req = new AttributeRequest();
+ req.TypeUri = "http://someType";
+ req.Count = 1;
+ req.Respond(null);
+ }
+ }
+}
diff --git a/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchResponseTests.cs b/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchResponseTests.cs new file mode 100644 index 0000000..14533ce --- /dev/null +++ b/src/DotNetOpenId.Test/Extensions/AttributeExchangeFetchResponseTests.cs @@ -0,0 +1,52 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using DotNetOpenId.Extensions;
+
+namespace DotNetOpenId.Test.Extensions {
+ [TestFixture]
+ public class AttributeExchangeFetchResponseTests {
+ [Test]
+ public void AddAttribute() {
+ var response = new AttributeExchangeFetchResponse();
+ response.AddAttribute(new AttributeResponse {
+ TypeUri = "http://someattribute",
+ Values = new[] { "Value1" },
+ });
+ }
+
+ [Test]
+ public void AddTwoAttributes() {
+ var response = new AttributeExchangeFetchResponse();
+ response.AddAttribute(new AttributeResponse {
+ TypeUri = "http://someattribute",
+ Values = new[] { "Value1" },
+ });
+ response.AddAttribute(new AttributeResponse {
+ TypeUri = "http://someOtherAttribute",
+ Values = new[] { "Value2" },
+ });
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void AddAttributeTwice() {
+ var response = new AttributeExchangeFetchResponse();
+ response.AddAttribute(new AttributeResponse {
+ TypeUri = "http://someattribute",
+ Values = new[] { "Value1" },
+ });
+ response.AddAttribute(new AttributeResponse {
+ TypeUri = "http://someattribute",
+ Values = new[] { "Value1" },
+ });
+ }
+
+ [Test, ExpectedException(typeof(ArgumentNullException))]
+ public void AddAttributeNull() {
+ var response = new AttributeExchangeFetchResponse();
+ response.AddAttribute(null);
+ }
+ }
+}
diff --git a/src/DotNetOpenId.Test/Extensions/AttributeExchangeTests.cs b/src/DotNetOpenId.Test/Extensions/AttributeExchangeTests.cs index c216fd1..ab28784 100644 --- a/src/DotNetOpenId.Test/Extensions/AttributeExchangeTests.cs +++ b/src/DotNetOpenId.Test/Extensions/AttributeExchangeTests.cs @@ -9,6 +9,9 @@ using DotNetOpenId.Extensions; namespace DotNetOpenId.Test.Extensions {
[TestFixture]
public class AttributeExchangeTests : ExtensionTestBase {
+ const string nicknameTypeUri = "http://axschema.org/namePerson/friendly";
+ const string emailTypeUri = "http://axschema.org/contact/email";
+
[Test]
public void None() {
var fetchResponse = ParameterizedTest<AttributeExchangeFetchResponse>(
@@ -22,9 +25,36 @@ namespace DotNetOpenId.Test.Extensions { [Test]
public void Fetch() {
var request = new AttributeExchangeFetchRequest();
+ request.AddAttribute(new AttributeRequest { TypeUri = nicknameTypeUri });
+ request.AddAttribute(new AttributeRequest { TypeUri = emailTypeUri, Count = int.MaxValue });
+ var response = ParameterizedTest<AttributeExchangeFetchResponse>(
+ TestSupport.GetIdentityUrl(TestSupport.Scenarios.ExtensionFullCooperation, Version), request);
+ Assert.IsNotNull(response);
+ var att = response.GetAttribute(nicknameTypeUri);
+ Assert.IsNotNull(att);
+ Assert.AreEqual(nicknameTypeUri, att.TypeUri);
+ Assert.AreEqual(1, att.Values.Length);
+ Assert.AreEqual("Andrew", att.Values[0]);
+ att = response.GetAttribute(emailTypeUri);
+ Assert.IsNotNull(att);
+ Assert.AreEqual(emailTypeUri, att.TypeUri);
+ Assert.AreEqual(2, att.Values.Length);
+ Assert.AreEqual("a@a.com", att.Values[0]);
+ Assert.AreEqual("b@b.com", att.Values[1]);
+ }
+
+ [Test]
+ public void FetchLimitEmails() {
+ var request = new AttributeExchangeFetchRequest();
+ request.AddAttribute(new AttributeRequest { TypeUri = emailTypeUri, Count = 1 });
var response = ParameterizedTest<AttributeExchangeFetchResponse>(
TestSupport.GetIdentityUrl(TestSupport.Scenarios.ExtensionFullCooperation, Version), request);
Assert.IsNotNull(response);
+ var att = response.GetAttribute(emailTypeUri);
+ Assert.IsNotNull(att);
+ Assert.AreEqual(emailTypeUri, att.TypeUri);
+ Assert.AreEqual(1, att.Values.Length);
+ Assert.AreEqual("a@a.com", att.Values[0]);
}
[Test]
diff --git a/src/DotNetOpenId.Test/Extensions/ExtensionTestBase.cs b/src/DotNetOpenId.Test/Extensions/ExtensionTestBase.cs index adb93bb..3f14253 100644 --- a/src/DotNetOpenId.Test/Extensions/ExtensionTestBase.cs +++ b/src/DotNetOpenId.Test/Extensions/ExtensionTestBase.cs @@ -11,12 +11,12 @@ using System.Diagnostics; namespace DotNetOpenId.Test.Extensions {
public class ExtensionTestBase {
- protected IRelyingPartyApplicationStore Store;
+ protected IRelyingPartyApplicationStore AppStore;
protected const ProtocolVersion Version = ProtocolVersion.V20;
[SetUp]
public virtual void Setup() {
- Store = new ApplicationMemoryStore();
+ AppStore = new ApplicationMemoryStore();
}
protected T ParameterizedTest<T>(Identifier identityUrl, IExtensionRequest extensionArgs)
@@ -24,7 +24,7 @@ namespace DotNetOpenId.Test.Extensions { Debug.Assert(identityUrl != null);
var returnTo = TestSupport.GetFullUrl(TestSupport.ConsumerPage);
var realm = new Realm(TestSupport.GetFullUrl(TestSupport.ConsumerPage).AbsoluteUri);
- var consumer = new OpenIdRelyingParty(Store, null);
+ var consumer = new OpenIdRelyingParty(AppStore, null);
var request = consumer.CreateRequest(identityUrl, realm, returnTo);
if (extensionArgs != null)
extensionArgs.AddToRequest(request);
@@ -46,7 +46,7 @@ namespace DotNetOpenId.Test.Extensions { }
throw;
}
- consumer = new OpenIdRelyingParty(Store, redirectUrl);
+ consumer = new OpenIdRelyingParty(AppStore, redirectUrl);
Assert.AreEqual(AuthenticationStatus.Authenticated, consumer.Response.Status);
Assert.AreEqual(identityUrl, consumer.Response.ClaimedIdentifier);
T r = new T();
diff --git a/src/DotNetOpenId.TestWeb/ProviderEndpoint.aspx.cs b/src/DotNetOpenId.TestWeb/ProviderEndpoint.aspx.cs index 10cb735..40f21a9 100644 --- a/src/DotNetOpenId.TestWeb/ProviderEndpoint.aspx.cs +++ b/src/DotNetOpenId.TestWeb/ProviderEndpoint.aspx.cs @@ -12,9 +12,9 @@ using System.Collections.Specialized; using DotNetOpenId.Extensions;
public partial class ProviderEndpoint : System.Web.UI.Page {
- protected void Page_Load(object sender, EventArgs e) {
+ const string nicknameTypeUri = "http://axschema.org/namePerson/friendly";
+ const string emailTypeUri = "http://axschema.org/contact/email";
- }
void respondToExtensions(DotNetOpenId.Provider.IRequest request, TestSupport.Scenarios scenario) {
var sregRequest = SimpleRegistrationRequestFields.ReadFromRequest(request);
var sregResponse = new SimpleRegistrationFieldValues();
@@ -24,19 +24,47 @@ public partial class ProviderEndpoint : System.Web.UI.Page { var aeStoreResponse = new AttributeExchangeStoreResponse();
switch (scenario) {
case TestSupport.Scenarios.ExtensionFullCooperation:
- if (sregRequest.FullName != SimpleRegistrationRequest.NoRequest)
- sregResponse.FullName = "Andrew Arnott";
- if (sregRequest.Email != SimpleRegistrationRequest.NoRequest)
- sregResponse.Email = "andrewarnott@gmail.com";
+ if (sregRequest != null) {
+ if (sregRequest.FullName != SimpleRegistrationRequest.NoRequest)
+ sregResponse.FullName = "Andrew Arnott";
+ if (sregRequest.Email != SimpleRegistrationRequest.NoRequest)
+ sregResponse.Email = "andrewarnott@gmail.com";
+ }
+ if (aeFetchRequest != null) {
+ var att = aeFetchRequest.GetAttribute(nicknameTypeUri);
+ if (att != null)
+ aeFetchResponse.AddAttribute(att.Respond("Andrew"));
+ att = aeFetchRequest.GetAttribute(emailTypeUri);
+ if (att != null) {
+ string[] emails = new[] { "a@a.com", "b@b.com" };
+ string[] subset = new string[Math.Min(emails.Length, att.Count)];
+ Array.Copy(emails, subset, subset.Length);
+ aeFetchResponse.AddAttribute(att.Respond(subset));
+ }
+ }
break;
case TestSupport.Scenarios.ExtensionPartialCooperation:
- if (sregRequest.FullName == SimpleRegistrationRequest.Require)
- sregResponse.FullName = "Andrew Arnott";
- if (sregRequest.Email == SimpleRegistrationRequest.Require)
- sregResponse.Email = "andrewarnott@gmail.com";
+ if (sregRequest != null) {
+ if (sregRequest.FullName == SimpleRegistrationRequest.Require)
+ sregResponse.FullName = "Andrew Arnott";
+ if (sregRequest.Email == SimpleRegistrationRequest.Require)
+ sregResponse.Email = "andrewarnott@gmail.com";
+ }
+ if (aeFetchRequest != null) {
+ var att = aeFetchRequest.GetAttribute(nicknameTypeUri);
+ if (att != null && att.IsRequired)
+ aeFetchResponse.AddAttribute(att.Respond("Andrew"));
+ att = aeFetchRequest.GetAttribute(emailTypeUri);
+ if (att != null && att.IsRequired) {
+ string[] emails = new[] { "a@a.com", "b@b.com" };
+ string[] subset = new string[Math.Min(emails.Length, att.Count)];
+ Array.Copy(emails, subset, subset.Length);
+ aeFetchResponse.AddAttribute(att.Respond(subset));
+ }
+ }
break;
}
- sregResponse.AddToResponse(request);
+ if (sregRequest != null) sregResponse.AddToResponse(request);
if (aeFetchRequest != null) aeFetchResponse.AddToResponse(request);
if (aeStoreRequest != null) aeStoreResponse.AddToResponse(request);
}
diff --git a/src/DotNetOpenId/DotNetOpenId.csproj b/src/DotNetOpenId/DotNetOpenId.csproj index 696dee7..6b70277 100644 --- a/src/DotNetOpenId/DotNetOpenId.csproj +++ b/src/DotNetOpenId/DotNetOpenId.csproj @@ -52,11 +52,15 @@ <Compile Include="AssociationMemoryStore.cs" />
<Compile Include="Associations.cs" />
<Compile Include="ExtensionArgumentsManager.cs" />
+ <Compile Include="Extensions\AliasManager.cs" />
<Compile Include="Extensions\AttributeExchangeFetchResponse.cs" />
+ <Compile Include="Extensions\AttributeRequest.cs" />
+ <Compile Include="Extensions\AttributeResponse.cs" />
<Compile Include="Extensions\Constants.cs" />
<Compile Include="Extensions\AttributeExchangeFetchRequest.cs" />
<Compile Include="Extensions\AttributeExchangeStoreRequest.cs" />
<Compile Include="Extensions\AttributeExchangeStoreResponse.cs" />
+ <Compile Include="Extensions\DemandLevel.cs" />
<Compile Include="Extensions\IExtension.cs" />
<Compile Include="HmacSha256Association.cs" />
<Compile Include="Identifier.cs" />
diff --git a/src/DotNetOpenId/Extensions/AliasManager.cs b/src/DotNetOpenId/Extensions/AliasManager.cs new file mode 100644 index 0000000..70aca3c --- /dev/null +++ b/src/DotNetOpenId/Extensions/AliasManager.cs @@ -0,0 +1,65 @@ +using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace DotNetOpenId.Extensions {
+ class AliasManager {
+ readonly string aliasFormat = "alias{0}";
+ /// <summary>
+ /// Tracks extension Type URIs and aliases assigned to them.
+ /// </summary>
+ Dictionary<string, string> typeUriToAliasMap = new Dictionary<string, string>();
+ /// <summary>
+ /// Tracks extension aliases and Type URIs assigned to them.
+ /// </summary>
+ Dictionary<string, string> aliasToTypeUriMap = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Gets an alias assigned for a given Type URI. A new alias is assigned if necessary.
+ /// </summary>
+ public string GetAlias(string typeUri) {
+ if (string.IsNullOrEmpty(typeUri)) throw new ArgumentNullException("typeUri");
+ string alias;
+ if (typeUriToAliasMap.TryGetValue(typeUri, out alias))
+ return alias;
+ else
+ return assignNewAlias(typeUri);
+ }
+
+ /// <summary>
+ /// Sets an alias and the value that will be returned by <see cref="ResolveAlias"/>.
+ /// </summary>
+ public void SetAlias(string alias, string typeUri) {
+ if (string.IsNullOrEmpty(alias)) throw new ArgumentNullException("alias");
+ if (string.IsNullOrEmpty(typeUri)) throw new ArgumentNullException("typeUri");
+ aliasToTypeUriMap.Add(alias, typeUri);
+ typeUriToAliasMap.Add(typeUri, alias);
+ }
+
+ /// <summary>
+ /// Gets the Type Uri encoded by a given alias.
+ /// </summary>
+ public string ResolveAlias(string alias) {
+ if (string.IsNullOrEmpty(alias)) throw new ArgumentNullException("alias");
+ string typeUri;
+ if (!aliasToTypeUriMap.TryGetValue(alias, out typeUri))
+ throw new ArgumentOutOfRangeException("alias");
+ return typeUri;
+ }
+
+ public IEnumerable<string> Aliases {
+ get { return aliasToTypeUriMap.Keys; }
+ }
+
+ string assignNewAlias(string typeUri) {
+ Debug.Assert(!string.IsNullOrEmpty(typeUri));
+ Debug.Assert(!typeUriToAliasMap.ContainsKey(typeUri));
+ string alias = string.Format(CultureInfo.InvariantCulture, aliasFormat, typeUriToAliasMap.Count + 1);
+ typeUriToAliasMap.Add(typeUri, alias);
+ aliasToTypeUriMap.Add(alias, typeUri);
+ return alias;
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/AttributeExchangeFetchRequest.cs b/src/DotNetOpenId/Extensions/AttributeExchangeFetchRequest.cs index 245f19a..07d68c6 100644 --- a/src/DotNetOpenId/Extensions/AttributeExchangeFetchRequest.cs +++ b/src/DotNetOpenId/Extensions/AttributeExchangeFetchRequest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Text;
using System.Globalization;
+using System.Diagnostics;
namespace DotNetOpenId.Extensions {
/// <summary>
@@ -10,6 +11,32 @@ namespace DotNetOpenId.Extensions { public class AttributeExchangeFetchRequest : IExtensionRequest {
readonly string Mode = "fetch_request";
+ List<AttributeRequest> attributesRequested = new List<AttributeRequest>();
+ public void AddAttribute(AttributeRequest attribute) {
+ if (attribute == null) throw new ArgumentNullException("attribute");
+ if (containsAttribute(attribute.TypeUri)) throw new ArgumentException(
+ string.Format(CultureInfo.CurrentCulture,
+ Strings.AttributeAlreadyAdded, attribute.TypeUri), "attribute");
+ attributesRequested.Add(attribute);
+ }
+ public AttributeRequest GetAttribute(string typeUri) {
+ foreach (var attribute in attributesRequested)
+ if (string.Equals(attribute.TypeUri, typeUri, StringComparison.Ordinal))
+ return attribute;
+ return null;
+ }
+ bool containsAttribute(string typeUri) {
+ return GetAttribute(typeUri) != null;
+ }
+
+ /// <summary>
+ /// If set, the OpenID Provider may re-post the fetch response message to the
+ /// specified URL at some time after the initial response has been sent, using an
+ /// OpenID Authentication Positive Assertion to inform the relying party of updates
+ /// to the requested fields.
+ /// </summary>
+ public Uri UpdateUrl { get; set; }
+
/// <summary>
/// Reads an incoming authentication request (from a relying party)
/// for Attribute Exchange properties and returns an instance of this
@@ -27,6 +54,29 @@ namespace DotNetOpenId.Extensions { var fields = new Dictionary<string, string> {
{ "mode", Mode },
};
+ if (UpdateUrl != null)
+ fields.Add("update_url", UpdateUrl.AbsoluteUri);
+
+ List<string> requiredAliases = new List<string>(), optionalAliases = new List<string>();
+ AliasManager aliasManager = new AliasManager();
+ foreach (var att in attributesRequested) {
+ string alias = aliasManager.GetAlias(att.TypeUri);
+ // define the alias<->typeUri mapping
+ fields.Add("type." + alias, att.TypeUri);
+ // set how many values the relying party wants max
+ fields.Add("count." + alias, att.Count.ToString());
+ if (att.IsRequired)
+ requiredAliases.Add(alias);
+ else
+ optionalAliases.Add(alias);
+ }
+
+ // Set optional/required lists
+ if (optionalAliases.Count > 0)
+ fields.Add("if_available", string.Join(",", optionalAliases.ToArray()));
+ if (requiredAliases.Count > 0)
+ fields.Add("required", string.Join(",", requiredAliases.ToArray()));
+
authenticationRequest.AddExtensionArguments(Constants.ae.ns, fields);
}
@@ -37,9 +87,68 @@ namespace DotNetOpenId.Extensions { fields.TryGetValue("mode", out mode);
if (mode != Mode) return false;
+ string updateUrl;
+ fields.TryGetValue("update_url", out updateUrl);
+ Uri updateUri;
+ if (Uri.TryCreate(updateUrl, UriKind.Absolute, out updateUri))
+ UpdateUrl = updateUri;
+
+ string requiredAliasString, optionalAliasString;
+ fields.TryGetValue("if_available", out optionalAliasString);
+ fields.TryGetValue("required", out requiredAliasString);
+ var requiredAliases = parseAliasList(requiredAliasString);
+ var optionalAliases = parseAliasList(optionalAliasString);
+ // if an alias shows up in both lists, an exception will result implicitly.
+ var allAliases = new List<string>(requiredAliases.Count + optionalAliases.Count);
+ allAliases.AddRange(requiredAliases);
+ allAliases.AddRange(optionalAliases);
+ AliasManager aliasManager = new AliasManager();
+ foreach (var alias in allAliases) {
+ string typeUri;
+ if (fields.TryGetValue("type." + alias, out typeUri)) {
+ aliasManager.SetAlias(alias, typeUri);
+ AttributeRequest att = new AttributeRequest {
+ TypeUri = typeUri,
+ IsRequired = requiredAliases.Contains(alias),
+ };
+ string countString;
+ if (fields.TryGetValue("count." + alias, out countString)) {
+ if (countString == "unlimited")
+ att.Count = int.MaxValue;
+ else {
+ int count;
+ if (int.TryParse(countString, out count) && count > 0) {
+ att.Count = count;
+ } else {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("count." + alias + " could not be parsed into a positive integer.");
+ }
+ }
+ } else {
+ att.Count = 1;
+ }
+ AddAttribute(att);
+ } else {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Type URI definition of alias " + alias + " is missing.");
+ }
+ }
+
return true;
}
+ List<string> parseAliasList(string aliasList) {
+ List<string> result = new List<string>();
+ if (string.IsNullOrEmpty(aliasList)) return result;
+ if (aliasList.Contains(".") || aliasList.Contains("\n")) {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Illegal characters found in Attribute Exchange alias list.");
+ return result;
+ }
+ result.AddRange(aliasList.Split(','));
+ return result;
+ }
+
#endregion
}
}
diff --git a/src/DotNetOpenId/Extensions/AttributeExchangeFetchResponse.cs b/src/DotNetOpenId/Extensions/AttributeExchangeFetchResponse.cs index db72875..49ecdfd 100644 --- a/src/DotNetOpenId/Extensions/AttributeExchangeFetchResponse.cs +++ b/src/DotNetOpenId/Extensions/AttributeExchangeFetchResponse.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text;
using DotNetOpenId.RelyingParty;
using System.Globalization;
+using System.Diagnostics;
namespace DotNetOpenId.Extensions {
/// <summary>
@@ -10,7 +11,35 @@ namespace DotNetOpenId.Extensions { /// </summary>
public class AttributeExchangeFetchResponse : IExtensionResponse {
readonly string Mode = "fetch_response";
-
+
+ List<AttributeResponse> attributesProvided = new List<AttributeResponse>();
+ public void AddAttribute(AttributeResponse attribute) {
+ if (attribute == null) throw new ArgumentNullException("attribute");
+ if (containsAttribute(attribute.TypeUri)) throw new ArgumentException(
+ string.Format(CultureInfo.CurrentCulture, Strings.AttributeAlreadyAdded, attribute.TypeUri));
+ attributesProvided.Add(attribute);
+ }
+ public AttributeResponse GetAttribute(string typeUri) {
+ foreach (var att in attributesProvided) {
+ if (att.TypeUri == typeUri)
+ return att;
+ }
+ return null;
+ }
+ bool containsAttribute(string typeUri) {
+ return GetAttribute(typeUri) != null;
+ }
+
+ /// <summary>
+ /// Whether the OpenID Provider intends to honor the request for updates.
+ /// </summary>
+ public bool UpdateUrlSupported { get { return UpdateUrl != null; } }
+ /// <summary>
+ /// The URL the OpenID Provider will post updates to. Must be set if the Provider
+ /// supports and will use this feature.
+ /// </summary>
+ public Uri UpdateUrl { get; set; }
+
/// <summary>
/// Reads a Provider's response for Attribute Exchange values and returns
/// an instance of this struct with the values.
@@ -27,6 +56,25 @@ namespace DotNetOpenId.Extensions { var fields = new Dictionary<string, string> {
{ "mode", Mode },
};
+
+ if (UpdateUrlSupported)
+ fields.Add("update_url", UpdateUrl.AbsoluteUri);
+
+ AliasManager aliasManager = new AliasManager();
+ foreach (var att in attributesProvided) {
+ string alias = aliasManager.GetAlias(att.TypeUri);
+ if (att.Values.Length == 0) continue;
+ fields.Add("type." + alias, att.TypeUri);
+ if (att.Values.Length > 1) {
+ fields.Add("count." + alias, att.Values.Length.ToString());
+ for (int i = 0; i < att.Values.Length; i++) {
+ fields.Add(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i + 1), att.Values[i]);
+ }
+ } else {
+ fields.Add("value." + alias, att.Values[0]);
+ }
+ }
+
authenticationRequest.AddExtensionArguments(Constants.ae.ns, fields);
}
@@ -37,9 +85,72 @@ namespace DotNetOpenId.Extensions { fields.TryGetValue("mode", out mode);
if (mode != Mode) return false;
+ string updateUrl;
+ fields.TryGetValue("update_url", out updateUrl);
+ Uri updateUri;
+ if (Uri.TryCreate(updateUrl, UriKind.Absolute, out updateUri))
+ UpdateUrl = updateUri;
+
+ AliasManager aliasManager = parseAliases(fields);
+ foreach (string alias in aliasManager.Aliases) {
+ AttributeResponse att = new AttributeResponse() {
+ TypeUri = aliasManager.ResolveAlias(alias),
+ };
+ int count = 1;
+ bool countSent = false;
+ string countString;
+ if (fields.TryGetValue("count." + alias, out countString)) {
+ if (!int.TryParse(countString, out count) || count <= 0) {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Failed to parse count.{0} value to a positive integer.");
+ continue;
+ }
+ countSent = true;
+ }
+ att.Values = new string[count];
+ if (countSent) {
+ for (int i = 0; i < att.Values.Length; i++) {
+ string value;
+ if (fields.TryGetValue(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i + 1), out value)) {
+ att.Values[i] = value;
+ } else {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Missing value for attribute '{0}'.", att.TypeUri);
+ continue;
+ }
+ }
+ } else {
+ string value;
+ if (fields.TryGetValue("value." + alias, out value))
+ att.Values[0] = value;
+ else {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Missing value for attribute '{0}'.", att.TypeUri);
+ continue;
+ }
+ }
+ AddAttribute(att);
+ }
+
return true;
}
+ AliasManager parseAliases(IDictionary<string, string> fields) {
+ Debug.Assert(fields != null);
+ AliasManager aliasManager = new AliasManager();
+ foreach (var pair in fields) {
+ if (!pair.Key.StartsWith("type.", StringComparison.Ordinal)) continue;
+ string alias = pair.Key.Substring(5);
+ if (alias.IndexOfAny(new[] { '.', ',', ':' }) >= 0) {
+ if (TraceUtil.Switch.TraceError)
+ Trace.TraceError("Illegal characters in alias name '{0}'.", alias);
+ continue;
+ }
+ aliasManager.SetAlias(alias, pair.Value);
+ }
+ return aliasManager;
+ }
+
#endregion
}
}
diff --git a/src/DotNetOpenId/Extensions/AttributeRequest.cs b/src/DotNetOpenId/Extensions/AttributeRequest.cs new file mode 100644 index 0000000..4c8e947 --- /dev/null +++ b/src/DotNetOpenId/Extensions/AttributeRequest.cs @@ -0,0 +1,45 @@ +using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Globalization;
+
+namespace DotNetOpenId.Extensions {
+ /// <summary>
+ /// An individual attribute to be requested of the OpenID Provider using
+ /// the Attribute Exchange extension.
+ /// </summary>
+ public class AttributeRequest {
+ /// <summary>
+ /// The URI uniquely identifying the attribute being requested.
+ /// </summary>
+ public string TypeUri;
+ /// <summary>
+ /// Whether the relying party considers this a required field.
+ /// Note that even if set to true, the Provider may not provide the value.
+ /// </summary>
+ public bool IsRequired;
+ int count = 1;
+ /// <summary>
+ /// The maximum number of values for this attribute the
+ /// Relying Party wishes to receive from the OpenID Provider.
+ /// A value of int.MaxValue is considered infinity.
+ /// </summary>
+ public int Count {
+ get { return count; }
+ set {
+ if (value <= 0) throw new ArgumentOutOfRangeException("value");
+ count = value;
+ }
+ }
+
+ public AttributeResponse Respond(params string[] values) {
+ if (values == null) throw new ArgumentNullException("values");
+ if (values.Length > Count) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.AttributeTooManyValues, Count, TypeUri, values.Length));
+ return new AttributeResponse {
+ TypeUri = this.TypeUri,
+ Values = values,
+ };
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/AttributeResponse.cs b/src/DotNetOpenId/Extensions/AttributeResponse.cs new file mode 100644 index 0000000..cff7874 --- /dev/null +++ b/src/DotNetOpenId/Extensions/AttributeResponse.cs @@ -0,0 +1,23 @@ +using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.Extensions {
+ /// <summary>
+ /// An individual attribute's value(s) as supplied by an OpenID Provider
+ /// in response to a prior request by an OpenID Relying Party.
+ /// </summary>
+ public class AttributeResponse {
+ internal AttributeResponse() { }
+
+ /// <summary>
+ /// The URI uniquely identifying the attribute whose value is being supplied.
+ /// </summary>
+ public string TypeUri { get; internal set; }
+
+ /// <summary>
+ /// Gets the values supplied by the Provider.
+ /// </summary>
+ public string[] Values;
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/DemandLevel.cs b/src/DotNetOpenId/Extensions/DemandLevel.cs new file mode 100644 index 0000000..05ec8be --- /dev/null +++ b/src/DotNetOpenId/Extensions/DemandLevel.cs @@ -0,0 +1,21 @@ +using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.Extensions {
+ /// <summary>
+ /// Indicates a relying party's level of desire for a particular value
+ /// to be provided by the OpenID Provider.
+ /// </summary>
+ public enum DemandLevel {
+ /// <summary>
+ /// The relying party considers this information as optional.
+ /// </summary>
+ Optional,
+ /// <summary>
+ /// The relying party considers this information as required.
+ /// Note however, that the Provider still has the option to not supply this value.
+ /// </summary>
+ Required,
+ }
+}
diff --git a/src/DotNetOpenId/Strings.Designer.cs b/src/DotNetOpenId/Strings.Designer.cs index 549033e..cf6f6af 100644 --- a/src/DotNetOpenId/Strings.Designer.cs +++ b/src/DotNetOpenId/Strings.Designer.cs @@ -61,6 +61,24 @@ namespace DotNetOpenId { }
/// <summary>
+ /// Looks up a localized string similar to An attribute with type URI '{0}' has already been added..
+ /// </summary>
+ internal static string AttributeAlreadyAdded {
+ get {
+ return ResourceManager.GetString("AttributeAlreadyAdded", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Only {0} values for attribute '{1}' were requested, but {2} were supplied..
+ /// </summary>
+ internal static string AttributeTooManyValues {
+ get {
+ return ResourceManager.GetString("AttributeTooManyValues", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted..
/// </summary>
internal static string BadAssociationPrivateData {
diff --git a/src/DotNetOpenId/Strings.resx b/src/DotNetOpenId/Strings.resx index 9c11ef2..9e76b85 100644 --- a/src/DotNetOpenId/Strings.resx +++ b/src/DotNetOpenId/Strings.resx @@ -117,6 +117,12 @@ <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
+ <data name="AttributeAlreadyAdded" xml:space="preserve">
+ <value>An attribute with type URI '{0}' has already been added.</value>
+ </data>
+ <data name="AttributeTooManyValues" xml:space="preserve">
+ <value>Only {0} values for attribute '{1}' were requested, but {2} were supplied.</value>
+ </data>
<data name="BadAssociationPrivateData" xml:space="preserve">
<value>The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.</value>
</data>
|