summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/Extensions
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/Extensions')
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs187
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs45
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs144
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs156
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs114
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs18
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs285
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs207
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs127
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs164
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs323
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs223
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs190
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs32
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs48
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs34
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs57
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs22
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs81
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionsInteropHelper.cs124
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs70
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs77
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs69
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs60
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs65
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs222
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs282
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs316
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs357
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs46
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs32
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs70
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs97
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs34
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs25
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs197
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs28
37 files changed, 4628 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs
new file mode 100644
index 0000000..f6878ec
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs
@@ -0,0 +1,187 @@
+//-----------------------------------------------------------------------
+// <copyright file="AliasManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Manages a fast, two-way mapping between type URIs and their aliases.
+ /// </summary>
+ internal class AliasManager {
+ /// <summary>
+ /// The format of auto-generated aliases.
+ /// </summary>
+ private const string AliasFormat = "alias{0}";
+
+ /// <summary>
+ /// Tracks extension Type URIs and aliases assigned to them.
+ /// </summary>
+ private Dictionary<string, string> typeUriToAliasMap = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Tracks extension aliases and Type URIs assigned to them.
+ /// </summary>
+ private Dictionary<string, string> aliasToTypeUriMap = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Gets the aliases that have been set.
+ /// </summary>
+ public IEnumerable<string> Aliases {
+ get { return this.aliasToTypeUriMap.Keys; }
+ }
+
+ /// <summary>
+ /// Gets an alias assigned for a given Type URI. A new alias is assigned if necessary.
+ /// </summary>
+ /// <param name="typeUri">The type URI.</param>
+ /// <returns>The alias assigned to this type URI. Never null.</returns>
+ public string GetAlias(string typeUri) {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+ string alias;
+ return this.typeUriToAliasMap.TryGetValue(typeUri, out alias) ? alias : this.AssignNewAlias(typeUri);
+ }
+
+ /// <summary>
+ /// Sets an alias and the value that will be returned by <see cref="ResolveAlias"/>.
+ /// </summary>
+ /// <param name="alias">The alias.</param>
+ /// <param name="typeUri">The type URI.</param>
+ public void SetAlias(string alias, string typeUri) {
+ Requires.NotNullOrEmpty(alias, "alias");
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+ this.aliasToTypeUriMap.Add(alias, typeUri);
+ this.typeUriToAliasMap.Add(typeUri, alias);
+ }
+
+ /// <summary>
+ /// Takes a sequence of type URIs and assigns aliases for all of them.
+ /// </summary>
+ /// <param name="typeUris">The type URIs to create aliases for.</param>
+ /// <param name="preferredTypeUriToAliases">An optional dictionary of URI/alias pairs that suggest preferred aliases to use if available for certain type URIs.</param>
+ public void AssignAliases(IEnumerable<string> typeUris, IDictionary<string, string> preferredTypeUriToAliases) {
+ Requires.NotNull(typeUris, "typeUris");
+
+ // First go through the actually used type URIs and see which ones have matching preferred aliases.
+ if (preferredTypeUriToAliases != null) {
+ foreach (string typeUri in typeUris) {
+ if (this.typeUriToAliasMap.ContainsKey(typeUri)) {
+ // this Type URI is already mapped to an alias.
+ continue;
+ }
+
+ string preferredAlias;
+ if (preferredTypeUriToAliases.TryGetValue(typeUri, out preferredAlias) && !this.IsAliasUsed(preferredAlias)) {
+ this.SetAlias(preferredAlias, typeUri);
+ }
+ }
+ }
+
+ // Now go through the whole list again and assign whatever is left now that the preferred ones
+ // have gotten their picks where available.
+ foreach (string typeUri in typeUris) {
+ if (this.typeUriToAliasMap.ContainsKey(typeUri)) {
+ // this Type URI is already mapped to an alias.
+ continue;
+ }
+
+ this.AssignNewAlias(typeUri);
+ }
+ }
+
+ /// <summary>
+ /// Sets up aliases for any Type URIs in a dictionary that do not yet have aliases defined,
+ /// and where the given preferred alias is still available.
+ /// </summary>
+ /// <param name="preferredTypeUriToAliases">A dictionary of type URI keys and alias values.</param>
+ public void SetPreferredAliasesWhereNotSet(IDictionary<string, string> preferredTypeUriToAliases) {
+ Requires.NotNull(preferredTypeUriToAliases, "preferredTypeUriToAliases");
+
+ foreach (var pair in preferredTypeUriToAliases) {
+ if (this.typeUriToAliasMap.ContainsKey(pair.Key)) {
+ // type URI is already mapped
+ continue;
+ }
+
+ if (this.aliasToTypeUriMap.ContainsKey(pair.Value)) {
+ // alias is already mapped
+ continue;
+ }
+
+ // The type URI and alias are as yet unset, so go ahead and assign them.
+ this.SetAlias(pair.Value, pair.Key);
+ }
+ }
+
+ /// <summary>
+ /// Gets the Type Uri encoded by a given alias.
+ /// </summary>
+ /// <param name="alias">The alias.</param>
+ /// <returns>The Type URI.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown if the given alias does not have a matching TypeURI.</exception>
+ public string ResolveAlias(string alias) {
+ Requires.NotNullOrEmpty(alias, "alias");
+ string typeUri = this.TryResolveAlias(alias);
+ if (typeUri == null) {
+ throw new ArgumentOutOfRangeException("alias");
+ }
+ return typeUri;
+ }
+
+ /// <summary>
+ /// Gets the Type Uri encoded by a given alias.
+ /// </summary>
+ /// <param name="alias">The alias.</param>
+ /// <returns>The Type URI for the given alias, or null if none for that alias exist.</returns>
+ public string TryResolveAlias(string alias) {
+ Requires.NotNullOrEmpty(alias, "alias");
+ string typeUri = null;
+ this.aliasToTypeUriMap.TryGetValue(alias, out typeUri);
+ return typeUri;
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether an alias has already been assigned to a type URI.
+ /// </summary>
+ /// <param name="alias">The alias in question.</param>
+ /// <returns>True if the alias has already been assigned. False otherwise.</returns>
+ public bool IsAliasUsed(string alias) {
+ Requires.NotNullOrEmpty(alias, "alias");
+ return this.aliasToTypeUriMap.ContainsKey(alias);
+ }
+
+ /// <summary>
+ /// Determines whether a given TypeURI has an associated alias assigned to it.
+ /// </summary>
+ /// <param name="typeUri">The type URI.</param>
+ /// <returns>
+ /// <c>true</c> if the given type URI already has an alias assigned; <c>false</c> otherwise.
+ /// </returns>
+ public bool IsAliasAssignedTo(string typeUri) {
+ Requires.NotNull(typeUri, "typeUri");
+ return this.typeUriToAliasMap.ContainsKey(typeUri);
+ }
+
+ /// <summary>
+ /// Assigns a new alias to a given Type URI.
+ /// </summary>
+ /// <param name="typeUri">The type URI to assign a new alias to.</param>
+ /// <returns>The newly generated alias.</returns>
+ private string AssignNewAlias(string typeUri) {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+ ErrorUtilities.VerifyInternal(!this.typeUriToAliasMap.ContainsKey(typeUri), "Oops! This type URI already has an alias!");
+ string alias = string.Format(CultureInfo.InvariantCulture, AliasFormat, this.typeUriToAliasMap.Count + 1);
+ this.typeUriToAliasMap.Add(typeUri, alias);
+ this.aliasToTypeUriMap.Add(alias, typeUri);
+ return alias;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs
new file mode 100644
index 0000000..decd296
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+// <copyright file="AXAttributeFormats.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+
+ /// <summary>
+ /// The various Type URI formats an AX attribute may use by various remote parties.
+ /// </summary>
+ [Flags]
+ public enum AXAttributeFormats {
+ /// <summary>
+ /// No attribute format.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// AX attributes should use the Type URI format starting with <c>http://axschema.org/</c>.
+ /// </summary>
+ AXSchemaOrg = 0x1,
+
+ /// <summary>
+ /// AX attributes should use the Type URI format starting with <c>http://schema.openid.net/</c>.
+ /// </summary>
+ SchemaOpenIdNet = 0x2,
+
+ /// <summary>
+ /// AX attributes should use the Type URI format starting with <c>http://openid.net/schema/</c>.
+ /// </summary>
+ OpenIdNetSchema = 0x4,
+
+ /// <summary>
+ /// All known schemas.
+ /// </summary>
+ All = AXSchemaOrg | SchemaOpenIdNet | OpenIdNetSchema,
+
+ /// <summary>
+ /// The most common schemas.
+ /// </summary>
+ Common = AXSchemaOrg | SchemaOpenIdNet,
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs
new file mode 100644
index 0000000..96cc437
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------
+// <copyright file="AXUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Helper methods shared by multiple messages in the Attribute Exchange extension.
+ /// </summary>
+ public static class AXUtilities {
+ /// <summary>
+ /// Adds a request for an attribute considering it 'required'.
+ /// </summary>
+ /// <param name="collection">The attribute request collection.</param>
+ /// <param name="typeUri">The type URI of the required attribute.</param>
+ public static void AddRequired(this ICollection<AttributeRequest> collection, string typeUri) {
+ Requires.NotNull(collection, "collection");
+ collection.Add(new AttributeRequest(typeUri, true));
+ }
+
+ /// <summary>
+ /// Adds a request for an attribute without considering it 'required'.
+ /// </summary>
+ /// <param name="collection">The attribute request collection.</param>
+ /// <param name="typeUri">The type URI of the requested attribute.</param>
+ public static void AddOptional(this ICollection<AttributeRequest> collection, string typeUri) {
+ Requires.NotNull(collection, "collection");
+ collection.Add(new AttributeRequest(typeUri, false));
+ }
+
+ /// <summary>
+ /// Adds a given attribute with one or more values to the request for storage.
+ /// Applicable to Relying Parties only.
+ /// </summary>
+ /// <param name="collection">The collection of <see cref="AttributeValues"/> to add to.</param>
+ /// <param name="typeUri">The type URI of the attribute.</param>
+ /// <param name="values">The attribute values.</param>
+ public static void Add(this ICollection<AttributeValues> collection, string typeUri, params string[] values) {
+ Requires.NotNull(collection, "collection");
+ collection.Add(new AttributeValues(typeUri, values));
+ }
+
+ /// <summary>
+ /// Serializes a set of attribute values to a dictionary of fields to send in the message.
+ /// </summary>
+ /// <param name="fields">The dictionary to fill with serialized attributes.</param>
+ /// <param name="attributes">The attributes.</param>
+ internal static void SerializeAttributes(IDictionary<string, string> fields, IEnumerable<AttributeValues> attributes) {
+ Requires.NotNull(fields, "fields");
+ Requires.NotNull(attributes, "attributes");
+
+ AliasManager aliasManager = new AliasManager();
+ foreach (var att in attributes) {
+ string alias = aliasManager.GetAlias(att.TypeUri);
+ fields.Add("type." + alias, att.TypeUri);
+ if (att.Values == null) {
+ continue;
+ }
+ if (att.Values.Count != 1) {
+ fields.Add("count." + alias, att.Values.Count.ToString(CultureInfo.InvariantCulture));
+ for (int i = 0; i < att.Values.Count; i++) {
+ fields.Add(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i + 1), att.Values[i]);
+ }
+ } else {
+ fields.Add("value." + alias, att.Values[0]);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Deserializes attribute values from an incoming set of message data.
+ /// </summary>
+ /// <param name="fields">The data coming in with the message.</param>
+ /// <returns>The attribute values found in the message.</returns>
+ internal static IEnumerable<AttributeValues> DeserializeAttributes(IDictionary<string, string> fields) {
+ AliasManager aliasManager = ParseAliases(fields);
+ foreach (string alias in aliasManager.Aliases) {
+ AttributeValues att = new AttributeValues(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) {
+ Logger.OpenId.ErrorFormat("Failed to parse count.{0} value to a non-negative integer.", alias);
+ continue;
+ }
+ countSent = true;
+ }
+ if (countSent) {
+ for (int i = 1; i <= count; i++) {
+ string value;
+ if (fields.TryGetValue(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i), out value)) {
+ att.Values.Add(value);
+ } else {
+ Logger.OpenId.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
+ continue;
+ }
+ }
+ } else {
+ string value;
+ if (fields.TryGetValue("value." + alias, out value)) {
+ att.Values.Add(value);
+ } else {
+ Logger.OpenId.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
+ continue;
+ }
+ }
+ yield return att;
+ }
+ }
+
+ /// <summary>
+ /// Reads through the attributes included in the response to discover
+ /// the alias-TypeURI relationships.
+ /// </summary>
+ /// <param name="fields">The data included in the extension message.</param>
+ /// <returns>The alias manager that provides lookup between aliases and type URIs.</returns>
+ private static AliasManager ParseAliases(IDictionary<string, string> fields) {
+ Requires.NotNull(fields, "fields");
+
+ AliasManager aliasManager = new AliasManager();
+ const string TypePrefix = "type.";
+ foreach (var pair in fields) {
+ if (!pair.Key.StartsWith(TypePrefix, StringComparison.Ordinal)) {
+ continue;
+ }
+ string alias = pair.Key.Substring(TypePrefix.Length);
+ if (alias.IndexOfAny(FetchRequest.IllegalAliasCharacters) >= 0) {
+ Logger.OpenId.ErrorFormat("Illegal characters in alias name '{0}'.", alias);
+ continue;
+ }
+ aliasManager.SetAlias(alias, pair.Value);
+ }
+ return aliasManager;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs
new file mode 100644
index 0000000..cbcbff6
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs
@@ -0,0 +1,156 @@
+//-----------------------------------------------------------------------
+// <copyright file="AttributeRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An individual attribute to be requested of the OpenID Provider using
+ /// the Attribute Exchange extension.
+ /// </summary>
+ [Serializable]
+ [DebuggerDisplay("{TypeUri} (required: {IsRequired}) ({Count})")]
+ public class AttributeRequest {
+ /// <summary>
+ /// Backing field for the <see cref="Count"/> property.
+ /// </summary>
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int count = 1;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeRequest"/> class
+ /// with <see cref="IsRequired"/> = false, <see cref="Count"/> = 1.
+ /// </summary>
+ public AttributeRequest() {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeRequest"/> class
+ /// with <see cref="IsRequired"/> = false, <see cref="Count"/> = 1.
+ /// </summary>
+ /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param>
+ public AttributeRequest(string typeUri) {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+ this.TypeUri = typeUri;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeRequest"/> class
+ /// with <see cref="Count"/> = 1.
+ /// </summary>
+ /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param>
+ /// <param name="isRequired">A value indicating whether the Relying Party considers this attribute to be required for registration.</param>
+ public AttributeRequest(string typeUri, bool isRequired)
+ : this(typeUri) {
+ this.IsRequired = isRequired;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeRequest"/> class.
+ /// </summary>
+ /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param>
+ /// <param name="isRequired">A value indicating whether the Relying Party considers this attribute to be required for registration.</param>
+ /// <param name="count">The maximum number of values for this attribute the Relying Party is prepared to receive.</param>
+ public AttributeRequest(string typeUri, bool isRequired, int count)
+ : this(typeUri, isRequired) {
+ this.Count = count;
+ }
+
+ /// <summary>
+ /// Gets or sets the URI uniquely identifying the attribute being requested.
+ /// </summary>
+ public string TypeUri { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating 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 { get; set; }
+
+ /// <summary>
+ /// Gets or sets 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 this.count;
+ }
+
+ set {
+ Requires.InRange(value > 0, "value");
+ this.count = value;
+ }
+ }
+
+ /// <summary>
+ /// Used by a Provider to create a response to a request for an attribute's value(s)
+ /// using a given array of strings.
+ /// </summary>
+ /// <param name="values">The values for the requested attribute.</param>
+ /// <returns>
+ /// The newly created <see cref="AttributeValues"/> object that should be added to
+ /// the <see cref="FetchResponse"/> object.
+ /// </returns>
+ public AttributeValues Respond(params string[] values) {
+ Requires.NotNull(values, "values");
+ Requires.True(values.Length <= this.Count, "values");
+ return new AttributeValues(this.TypeUri, values);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ AttributeRequest other = obj as AttributeRequest;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.TypeUri != other.TypeUri) {
+ return false;
+ }
+
+ if (this.Count != other.Count) {
+ return false;
+ }
+
+ if (this.IsRequired != other.IsRequired) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ int hashCode = this.IsRequired ? 1 : 0;
+ unchecked {
+ hashCode += this.Count;
+ if (this.TypeUri != null) {
+ hashCode += this.TypeUri.GetHashCode();
+ }
+ }
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs
new file mode 100644
index 0000000..37ebe38
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs
@@ -0,0 +1,114 @@
+//-----------------------------------------------------------------------
+// <copyright file="AttributeValues.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An individual attribute's value(s) as supplied by an OpenID Provider
+ /// in response to a prior request by an OpenID Relying Party as part of
+ /// a fetch request, or by a relying party as part of a store request.
+ /// </summary>
+ [Serializable]
+ [DebuggerDisplay("{TypeUri}")]
+ public class AttributeValues {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeValues"/> class.
+ /// </summary>
+ /// <param name="typeUri">The TypeURI that uniquely identifies the attribute.</param>
+ /// <param name="values">The values for the attribute.</param>
+ public AttributeValues(string typeUri, params string[] values) {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+
+ this.TypeUri = typeUri;
+ this.Values = (IList<string>)values ?? EmptyList<string>.Instance;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeValues"/> class.
+ /// </summary>
+ /// <remarks>
+ /// This is internal because web sites should be using the
+ /// <see cref="AttributeRequest.Respond"/> method to instantiate.
+ /// </remarks>
+ internal AttributeValues() {
+ this.Values = new List<string>(1);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttributeValues"/> class.
+ /// </summary>
+ /// <param name="typeUri">The TypeURI of the attribute whose values are being provided.</param>
+ internal AttributeValues(string typeUri) {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+
+ this.TypeUri = typeUri;
+ this.Values = new List<string>(1);
+ }
+
+ /// <summary>
+ /// Gets 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 IList<string> Values { get; private set; }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ AttributeValues other = obj as AttributeValues;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.TypeUri != other.TypeUri) {
+ return false;
+ }
+
+ if (!MessagingUtilities.AreEquivalent<string>(this.Values, other.Values)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ int hashCode = 0;
+ unchecked {
+ if (this.TypeUri != null) {
+ hashCode += this.TypeUri.GetHashCode();
+ }
+
+ foreach (string value in this.Values) {
+ hashCode += value.GetHashCode();
+ }
+ }
+
+ return hashCode;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs
new file mode 100644
index 0000000..167d0d2
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs
@@ -0,0 +1,18 @@
+//-----------------------------------------------------------------------
+// <copyright file="Constants.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ /// <summary>
+ /// Attribute Exchange constants
+ /// </summary>
+ internal static class Constants {
+ /// <summary>
+ /// The TypeURI by which the AX extension is recognized in
+ /// OpenID messages and in XRDS documents.
+ /// </summary>
+ internal const string TypeUri = "http://openid.net/srv/ax/1.0";
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs
new file mode 100644
index 0000000..124a18c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs
@@ -0,0 +1,285 @@
+//-----------------------------------------------------------------------
+// <copyright file="FetchRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The Attribute Exchange Fetch message, request leg.
+ /// </summary>
+ [Serializable]
+ public sealed class FetchRequest : ExtensionBase, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && isProviderRole) {
+ string mode;
+ if (data.TryGetValue("mode", out mode) && mode == Mode) {
+ return new FetchRequest();
+ }
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// Characters that may not appear in an attribute alias list.
+ /// </summary>
+ internal static readonly char[] IllegalAliasListCharacters = new[] { '.', '\n' };
+
+ /// <summary>
+ /// Characters that may not appear in an attribute Type URI alias.
+ /// </summary>
+ internal static readonly char[] IllegalAliasCharacters = new[] { '.', ',', ':' };
+
+ /// <summary>
+ /// The value for the 'mode' parameter.
+ /// </summary>
+ [MessagePart("mode", IsRequired = true)]
+ private const string Mode = "fetch_request";
+
+ /// <summary>
+ /// The collection of requested attributes.
+ /// </summary>
+ private readonly KeyedCollection<string, AttributeRequest> attributes = new KeyedCollectionDelegate<string, AttributeRequest>(ar => ar.TypeUri);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FetchRequest"/> class.
+ /// </summary>
+ public FetchRequest()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+
+ /// <summary>
+ /// Gets a collection of the attributes whose values are
+ /// requested by the Relying Party.
+ /// </summary>
+ /// <value>A collection where the keys are the attribute type URIs, and the value
+ /// is all the attribute request details.</value>
+ public KeyedCollection<string, AttributeRequest> Attributes {
+ get {
+ Contract.Ensures(Contract.Result<KeyedCollection<string, AttributeRequest>>() != null);
+ return this.attributes;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL that the OpenID Provider may re-post the fetch response
+ /// message to 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>
+ [MessagePart("update_url", IsRequired = false)]
+ public Uri UpdateUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a list of aliases for optional attributes.
+ /// </summary>
+ /// <value>A comma-delimited list of aliases.</value>
+ [MessagePart("if_available", IsRequired = false)]
+ private string OptionalAliases { get; set; }
+
+ /// <summary>
+ /// Gets or sets a list of aliases for required attributes.
+ /// </summary>
+ /// <value>A comma-delimited list of aliases.</value>
+ [MessagePart("required", IsRequired = false)]
+ private string RequiredAliases { get; set; }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ FetchRequest other = obj as FetchRequest;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.Version != other.Version) {
+ return false;
+ }
+
+ if (this.UpdateUrl != other.UpdateUrl) {
+ return false;
+ }
+
+ if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ unchecked {
+ int hashCode = this.Version.GetHashCode();
+
+ if (this.UpdateUrl != null) {
+ hashCode += this.UpdateUrl.GetHashCode();
+ }
+
+ foreach (AttributeRequest att in this.Attributes) {
+ hashCode += att.GetHashCode();
+ }
+
+ return hashCode;
+ }
+ }
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ var fields = ((IMessage)this).ExtraData;
+ fields.Clear();
+
+ List<string> requiredAliases = new List<string>(), optionalAliases = new List<string>();
+ AliasManager aliasManager = new AliasManager();
+ foreach (var att in this.attributes) {
+ 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(CultureInfo.InvariantCulture));
+
+ if (att.IsRequired) {
+ requiredAliases.Add(alias);
+ } else {
+ optionalAliases.Add(alias);
+ }
+ }
+
+ // Set optional/required lists
+ this.OptionalAliases = optionalAliases.Count > 0 ? string.Join(",", optionalAliases.ToArray()) : null;
+ this.RequiredAliases = requiredAliases.Count > 0 ? string.Join(",", requiredAliases.ToArray()) : null;
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ var extraData = ((IMessage)this).ExtraData;
+ var requiredAliases = ParseAliasList(this.RequiredAliases);
+ var optionalAliases = ParseAliasList(this.OptionalAliases);
+
+ // 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);
+ if (allAliases.Count == 0) {
+ Logger.OpenId.Error("Attribute Exchange extension did not provide any aliases in the if_available or required lists.");
+ return;
+ }
+
+ AliasManager aliasManager = new AliasManager();
+ foreach (var alias in allAliases) {
+ string attributeTypeUri;
+ if (extraData.TryGetValue("type." + alias, out attributeTypeUri)) {
+ aliasManager.SetAlias(alias, attributeTypeUri);
+ AttributeRequest att = new AttributeRequest {
+ TypeUri = attributeTypeUri,
+ IsRequired = requiredAliases.Contains(alias),
+ };
+ string countString;
+ if (extraData.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 {
+ Logger.OpenId.Error("count." + alias + " could not be parsed into a positive integer.");
+ }
+ }
+ } else {
+ att.Count = 1;
+ }
+ this.Attributes.Add(att);
+ } else {
+ Logger.OpenId.Error("Type URI definition of alias " + alias + " is missing.");
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ protected override void EnsureValidMessage() {
+ base.EnsureValidMessage();
+
+ if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) {
+ this.UpdateUrl = null;
+ Logger.OpenId.ErrorFormat("The AX fetch request update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl);
+ }
+
+ if (this.OptionalAliases != null) {
+ if (this.OptionalAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) {
+ Logger.OpenId.Error("Illegal characters found in Attribute Exchange if_available alias list. Ignoring value.");
+ this.OptionalAliases = null;
+ }
+ }
+
+ if (this.RequiredAliases != null) {
+ if (this.RequiredAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) {
+ Logger.OpenId.Error("Illegal characters found in Attribute Exchange required alias list. Ignoring value.");
+ this.RequiredAliases = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Splits a list of aliases by their commas.
+ /// </summary>
+ /// <param name="aliasList">The comma-delimited list of aliases. May be null or empty.</param>
+ /// <returns>The list of aliases. Never null, but may be empty.</returns>
+ private static IList<string> ParseAliasList(string aliasList) {
+ if (string.IsNullOrEmpty(aliasList)) {
+ return EmptyList<string>.Instance;
+ }
+
+ return aliasList.Split(',');
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs
new file mode 100644
index 0000000..14b1caa
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs
@@ -0,0 +1,207 @@
+//-----------------------------------------------------------------------
+// <copyright file="FetchResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The Attribute Exchange Fetch message, response leg.
+ /// </summary>
+ [Serializable]
+ public sealed class FetchResponse : ExtensionBase, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && !isProviderRole) {
+ string mode;
+ if (data.TryGetValue("mode", out mode) && mode == Mode) {
+ return new FetchResponse();
+ }
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The value of the 'mode' parameter.
+ /// </summary>
+ [MessagePart("mode", IsRequired = true)]
+ private const string Mode = "fetch_response";
+
+ /// <summary>
+ /// The collection of provided attributes. This field will never be null.
+ /// </summary>
+ private readonly KeyedCollection<string, AttributeValues> attributesProvided = new KeyedCollectionDelegate<string, AttributeValues>(av => av.TypeUri);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FetchResponse"/> class.
+ /// </summary>
+ public FetchResponse()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+
+ /// <summary>
+ /// Gets a sequence of the attributes whose values are provided by the OpenID Provider.
+ /// </summary>
+ public KeyedCollection<string, AttributeValues> Attributes {
+ get {
+ Contract.Ensures(Contract.Result<KeyedCollection<string, AttributeValues>>() != null);
+ return this.attributesProvided;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the OpenID Provider intends to
+ /// honor the request for updates.
+ /// </summary>
+ public bool UpdateUrlSupported {
+ get { return this.UpdateUrl != null; }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL the OpenID Provider will post updates to.
+ /// Must be set if the Provider supports and will use this feature.
+ /// </summary>
+ [MessagePart("update_url", IsRequired = false)]
+ public Uri UpdateUrl { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this extension is signed by the Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsSignedByProvider {
+ get { return this.IsSignedByRemoteParty; }
+ }
+
+ /// <summary>
+ /// Gets the first attribute value provided for a given attribute Type URI.
+ /// </summary>
+ /// <param name="typeUri">
+ /// The type URI of the attribute.
+ /// Usually a constant from <see cref="WellKnownAttributes"/>.</param>
+ /// <returns>
+ /// The first value provided for the attribute, or <c>null</c> if the attribute is missing or no values were provided.
+ /// </returns>
+ /// <remarks>
+ /// This is meant as a helper method for the common case of just wanting one attribute value.
+ /// For greater flexibility or to retrieve more than just the first value for an attribute,
+ /// use the <see cref="Attributes"/> collection directly.
+ /// </remarks>
+ public string GetAttributeValue(string typeUri) {
+ if (this.Attributes.Contains(typeUri)) {
+ return this.Attributes[typeUri].Values.FirstOrDefault();
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ FetchResponse other = obj as FetchResponse;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.Version != other.Version) {
+ return false;
+ }
+
+ if (this.UpdateUrl != other.UpdateUrl) {
+ return false;
+ }
+
+ if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ unchecked {
+ int hashCode = this.Version.GetHashCode();
+
+ if (this.UpdateUrl != null) {
+ hashCode += this.UpdateUrl.GetHashCode();
+ }
+
+ foreach (AttributeValues value in this.Attributes) {
+ hashCode += value.GetHashCode();
+ }
+
+ return hashCode;
+ }
+ }
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ var extraData = ((IMessage)this).ExtraData;
+ AXUtilities.SerializeAttributes(extraData, this.attributesProvided);
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ var extraData = ((IMessage)this).ExtraData;
+ foreach (var att in AXUtilities.DeserializeAttributes(extraData)) {
+ this.Attributes.Add(att);
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ protected override void EnsureValidMessage() {
+ base.EnsureValidMessage();
+
+ if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) {
+ this.UpdateUrl = null;
+ Logger.OpenId.ErrorFormat("The AX fetch response update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs
new file mode 100644
index 0000000..641b17a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs
@@ -0,0 +1,127 @@
+//-----------------------------------------------------------------------
+// <copyright file="StoreRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using System.Collections.ObjectModel;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The Attribute Exchange Store message, request leg.
+ /// </summary>
+ [Serializable]
+ public sealed class StoreRequest : ExtensionBase, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && isProviderRole) {
+ string mode;
+ if (data.TryGetValue("mode", out mode) && mode == Mode) {
+ return new StoreRequest();
+ }
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The value of the 'mode' parameter.
+ /// </summary>
+ [MessagePart("mode", IsRequired = true)]
+ private const string Mode = "store_request";
+
+ /// <summary>
+ /// The collection of provided attribute values. This field will never be null.
+ /// </summary>
+ private readonly KeyedCollection<string, AttributeValues> attributesProvided = new KeyedCollectionDelegate<string, AttributeValues>(av => av.TypeUri);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StoreRequest"/> class.
+ /// </summary>
+ public StoreRequest()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+
+ /// <summary>
+ /// Gets the collection of all the attributes that are included in the store request.
+ /// </summary>
+ public KeyedCollection<string, AttributeValues> Attributes {
+ get { return this.attributesProvided; }
+ }
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ var fields = ((IMessage)this).ExtraData;
+ fields.Clear();
+
+ AXUtilities.SerializeAttributes(fields, this.attributesProvided);
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ var fields = ((IMessage)this).ExtraData;
+ foreach (var att in AXUtilities.DeserializeAttributes(fields)) {
+ this.Attributes.Add(att);
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as StoreRequest;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.Version != other.Version) {
+ return false;
+ }
+
+ if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ unchecked {
+ int hashCode = this.Version.GetHashCode();
+ foreach (AttributeValues att in this.Attributes) {
+ hashCode += att.GetHashCode();
+ }
+ return hashCode;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs
new file mode 100644
index 0000000..ba7f091
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs
@@ -0,0 +1,164 @@
+//-----------------------------------------------------------------------
+// <copyright file="StoreResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The Attribute Exchange Store message, response leg.
+ /// </summary>
+ [Serializable]
+ public sealed class StoreResponse : ExtensionBase {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && !isProviderRole) {
+ string mode;
+ if (data.TryGetValue("mode", out mode) && (mode == SuccessMode || mode == FailureMode)) {
+ return new StoreResponse();
+ }
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The value of the mode parameter used to express a successful store operation.
+ /// </summary>
+ private const string SuccessMode = "store_response_success";
+
+ /// <summary>
+ /// The value of the mode parameter used to express a store operation failure.
+ /// </summary>
+ private const string FailureMode = "store_response_failure";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StoreResponse"/> class
+ /// to represent a successful store operation.
+ /// </summary>
+ public StoreResponse()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ this.Succeeded = true;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StoreResponse"/> class
+ /// to represent a failed store operation.
+ /// </summary>
+ /// <param name="failureReason">The reason for failure.</param>
+ public StoreResponse(string failureReason)
+ : this() {
+ this.Succeeded = false;
+ this.FailureReason = failureReason;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the storage request succeeded.
+ /// </summary>
+ /// <value>Defaults to <c>true</c>.</value>
+ public bool Succeeded {
+ get { return this.Mode == SuccessMode; }
+ set { this.Mode = value ? SuccessMode : FailureMode; }
+ }
+
+ /// <summary>
+ /// Gets or sets the reason for the failure, if applicable.
+ /// </summary>
+ [MessagePart("error", IsRequired = false)]
+ public string FailureReason { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this extension is signed by the Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsSignedByProvider {
+ get { return this.IsSignedByRemoteParty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the mode argument.
+ /// </summary>
+ /// <value>One of 'store_response_success' or 'store_response_failure'.</value>
+ [MessagePart("mode", IsRequired = true)]
+ private string Mode { get; set; }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as StoreResponse;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.Version != other.Version) {
+ return false;
+ }
+
+ if (this.Succeeded != other.Succeeded) {
+ return false;
+ }
+
+ if (this.FailureReason != other.FailureReason) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ unchecked {
+ int hashCode = this.Version.GetHashCode();
+ hashCode += this.Succeeded ? 0 : 1;
+ if (this.FailureReason != null) {
+ hashCode += this.FailureReason.GetHashCode();
+ }
+
+ return hashCode;
+ }
+ }
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ protected override void EnsureValidMessage() {
+ base.EnsureValidMessage();
+
+ ErrorUtilities.VerifyProtocol(
+ this.Mode == SuccessMode || this.Mode == FailureMode,
+ MessagingStrings.UnexpectedMessagePartValue,
+ "mode",
+ this.Mode);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs
new file mode 100644
index 0000000..5aa89c6
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs
@@ -0,0 +1,323 @@
+//-----------------------------------------------------------------------
+// <copyright file="WellKnownAttributes.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// Attribute types defined at http://www.axschema.org/types/.
+ /// </summary>
+ /// <remarks>
+ /// If you don't see what you need here, check that URL to see if any have been added.
+ /// You can use new ones directly without adding them to this class, and can even make
+ /// up your own if you expect the other end to understand what you make up.
+ /// </remarks>
+ public static class WellKnownAttributes {
+ /// <summary>
+ /// Inherent attributes about a personality such as gender and bio.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Person {
+ /// <summary>Gender, either "M" or "F"</summary>
+ /// <example>"M", "F"</example>
+ public const string Gender = "http://axschema.org/person/gender";
+
+ /// <summary>Biography (text)</summary>
+ /// <example>"I am the very model of a modern Major General."</example>
+ public const string Biography = "http://axschema.org/media/biography";
+ }
+
+ /// <summary>
+ /// Preferences such as language and timezone.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Preferences {
+ /// <summary>Preferred language, as per RFC4646</summary>
+ /// <example>"en-US"</example>
+ public const string Language = "http://axschema.org/pref/language";
+
+ /// <summary>Home time zone information (as specified in <a href="http://en.wikipedia.org/wiki/List_of_tz_zones_by_name">zoneinfo</a>)</summary>
+ /// <example>"America/Pacific"</example>
+ public const string TimeZone = "http://axschema.org/pref/timezone";
+ }
+
+ /// <summary>
+ /// The names a person goes by.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Name {
+ /// <summary>Subject's alias or "screen" name</summary>
+ /// <example>"Johnny5"</example>
+ public const string Alias = "http://axschema.org/namePerson/friendly";
+
+ /// <summary>Full name of subject</summary>
+ /// <example>"John Doe"</example>
+ public const string FullName = "http://axschema.org/namePerson";
+
+ /// <summary>Honorific prefix for the subject's name</summary>
+ /// <example>"Mr.", "Mrs.", "Dr."</example>
+ public const string Prefix = "http://axschema.org/namePerson/prefix";
+
+ /// <summary>First or given name of subject</summary>
+ /// <example>"John"</example>
+ public const string First = "http://axschema.org/namePerson/first";
+
+ /// <summary>Last name or surname of subject</summary>
+ /// <example>"Smith"</example>
+ public const string Last = "http://axschema.org/namePerson/last";
+
+ /// <summary>Middle name(s) of subject</summary>
+ /// <example>"Robert"</example>
+ public const string Middle = "http://axschema.org/namePerson/middle";
+
+ /// <summary>Suffix of subject's name</summary>
+ /// <example>"III", "Jr."</example>
+ public const string Suffix = "http://axschema.org/namePerson/suffix";
+ }
+
+ /// <summary>
+ /// Business affiliation.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Company {
+ /// <summary>Company name (employer)</summary>
+ /// <example>"Springfield Power"</example>
+ public const string CompanyName = "http://axschema.org/company/name";
+
+ /// <summary>Employee title</summary>
+ /// <example>"Engineer"</example>
+ public const string JobTitle = "http://axschema.org/company/title";
+ }
+
+ /// <summary>
+ /// Information about a person's birthdate.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class BirthDate {
+ /// <summary>Date of birth.</summary>
+ /// <example>"1979-01-01"</example>
+ public const string WholeBirthDate = "http://axschema.org/birthDate";
+
+ /// <summary>Year of birth (four digits)</summary>
+ /// <example>"1979"</example>
+ public const string Year = "http://axschema.org/birthDate/birthYear";
+
+ /// <summary>Month of birth (1-12)</summary>
+ /// <example>"05"</example>
+ public const string Month = "http://axschema.org/birthDate/birthMonth";
+
+ /// <summary>Day of birth</summary>
+ /// <example>"31"</example>
+ public const string DayOfMonth = "http://axschema.org/birthDate/birthday";
+ }
+
+ /// <summary>
+ /// Various ways to contact a person.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Contact {
+ /// <summary>Internet SMTP email address as per RFC2822</summary>
+ /// <example>"jsmith@isp.example.com"</example>
+ public const string Email = "http://axschema.org/contact/email";
+
+ /// <summary>
+ /// Various types of phone numbers.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Phone {
+ /// <summary>Main phone number (preferred)</summary>
+ /// <example>+1-800-555-1234</example>
+ public const string Preferred = "http://axschema.org/contact/phone/default";
+
+ /// <summary>Home phone number</summary>
+ /// <example>+1-800-555-1234</example>
+ public const string Home = "http://axschema.org/contact/phone/home";
+
+ /// <summary>Business phone number</summary>
+ /// <example>+1-800-555-1234</example>
+ public const string Work = "http://axschema.org/contact/phone/business";
+
+ /// <summary>Cellular (or mobile) phone number</summary>
+ /// <example>+1-800-555-1234</example>
+ public const string Mobile = "http://axschema.org/contact/phone/cell";
+
+ /// <summary>Fax number</summary>
+ /// <example>+1-800-555-1234</example>
+ public const string Fax = "http://axschema.org/contact/phone/fax";
+ }
+
+ /// <summary>
+ /// The many fields that make up an address.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class HomeAddress {
+ /// <summary>Home postal address: street number, name and apartment number</summary>
+ /// <example>"#42 135 East 1st Street"</example>
+ public const string StreetAddressLine1 = "http://axschema.org/contact/postalAddress/home";
+
+ /// <summary>"#42 135 East 1st Street"</summary>
+ /// <example>"Box 67"</example>
+ public const string StreetAddressLine2 = "http://axschema.org/contact/postalAddressAdditional/home";
+
+ /// <summary>Home city name</summary>
+ /// <example>"Vancouver"</example>
+ public const string City = "http://axschema.org/contact/city/home";
+
+ /// <summary>Home state or province name</summary>
+ /// <example>"BC"</example>
+ public const string State = "http://axschema.org/contact/state/home";
+
+ /// <summary>Home country code in ISO.3166.1988 (alpha 2) format</summary>
+ /// <example>"CA"</example>
+ public const string Country = "http://axschema.org/contact/country/home";
+
+ /// <summary>Home postal code; region specific format</summary>
+ /// <example>"V5A 4B2"</example>
+ public const string PostalCode = "http://axschema.org/contact/postalCode/home";
+ }
+
+ /// <summary>
+ /// The many fields that make up an address.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class WorkAddress {
+ /// <summary>Business postal address: street number, name and apartment number</summary>
+ /// <example>"#42 135 East 1st Street"</example>
+ public const string StreetAddressLine1 = "http://axschema.org/contact/postalAddress/business";
+
+ /// <summary>"#42 135 East 1st Street"</summary>
+ /// <example>"Box 67"</example>
+ public const string StreetAddressLine2 = "http://axschema.org/contact/postalAddressAdditional/business";
+
+ /// <summary>Business city name</summary>
+ /// <example>"Vancouver"</example>
+ public const string City = "http://axschema.org/contact/city/business";
+
+ /// <summary>Business state or province name</summary>
+ /// <example>"BC"</example>
+ public const string State = "http://axschema.org/contact/state/business";
+
+ /// <summary>Business country code in ISO.3166.1988 (alpha 2) format</summary>
+ /// <example>"CA"</example>
+ public const string Country = "http://axschema.org/contact/country/business";
+
+ /// <summary>Business postal code; region specific format</summary>
+ /// <example>"V5A 4B2"</example>
+ public const string PostalCode = "http://axschema.org/contact/postalCode/business";
+ }
+
+ /// <summary>
+ /// Various handles for instant message clients.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class IM {
+ /// <summary>AOL instant messaging service handle</summary>
+ /// <example>"jsmith421234"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "AOL", Justification = "By design")]
+ public const string AOL = "http://axschema.org/contact/IM/AIM";
+
+ /// <summary>ICQ instant messaging service handle</summary>
+ /// <example>"1234567"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ICQ", Justification = "By design")]
+ public const string ICQ = "http://axschema.org/contact/IM/ICQ";
+
+ /// <summary>MSN instant messaging service handle</summary>
+ /// <example>"jsmith42@hotmail.com"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "MSN", Justification = "By design")]
+ public const string MSN = "http://axschema.org/contact/IM/MSN";
+
+ /// <summary>Yahoo! instant messaging service handle</summary>
+ /// <example>"jsmith421234"</example>
+ public const string Yahoo = "http://axschema.org/contact/IM/Yahoo";
+
+ /// <summary>Jabber instant messaging service handle</summary>
+ /// <example>"jsmith@jabber.example.com"</example>
+ public const string Jabber = "http://axschema.org/contact/IM/Jabber";
+
+ /// <summary>Skype instant messaging service handle</summary>
+ /// <example>"jsmith42"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Skype", Justification = "By design")]
+ public const string Skype = "http://axschema.org/contact/IM/Skype";
+ }
+
+ /// <summary>
+ /// Various web addresses connected with this personality.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "By design"), SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Web {
+ /// <summary>Web site URL</summary>
+ /// <example>"http://example.com/~jsmith/"</example>
+ public const string Homepage = "http://axschema.org/contact/web/default";
+
+ /// <summary>Blog home page URL</summary>
+ /// <example>"http://example.com/jsmith_blog/"</example>
+ public const string Blog = "http://axschema.org/contact/web/blog";
+
+ /// <summary>LinkedIn URL</summary>
+ /// <example>"http://www.linkedin.com/pub/1/234/56"</example>
+ public const string LinkedIn = "http://axschema.org/contact/web/Linkedin";
+
+ /// <summary>Amazon URL</summary>
+ /// <example>"http://www.amazon.com/gp/pdp/profile/A24DLKJ825"</example>
+ public const string Amazon = "http://axschema.org/contact/web/Amazon";
+
+ /// <summary>Flickr URL</summary>
+ /// <example>"http://flickr.com/photos/jsmith42/"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Flickr", Justification = "By design")]
+ public const string Flickr = "http://axschema.org/contact/web/Flickr";
+
+ /// <summary>del.icio.us URL</summary>
+ /// <example>"http://del.icio.us/jsmith42"</example>
+ public const string Delicious = "http://axschema.org/contact/web/Delicious";
+ }
+ }
+
+ /// <summary>
+ /// Audio and images of this personality.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "By design"), SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Media {
+ /// <summary>Spoken name (web URL)</summary>
+ /// <example>"http://example.com/~jsmith/john_smith.wav"</example>
+ public const string SpokenName = "http://axschema.org/media/spokenname";
+
+ /// <summary>Audio greeting (web URL)</summary>
+ /// <example>"http://example.com/~jsmith/i_greet_you.wav"</example>
+ public const string AudioGreeting = "http://axschema.org/media/greeting/audio";
+
+ /// <summary>Video greeting (web URL)</summary>
+ /// <example>"http://example.com/~jsmith/i_greet_you.mov"</example>
+ public const string VideoGreeting = "http://axschema.org/media/greeting/video";
+
+ /// <summary>
+ /// Images of this personality.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")]
+ public static class Images {
+ /// <summary>Image (web URL); unspecified dimension</summary>
+ /// <example>"http://example.com/~jsmith/image.jpg"</example>
+ public const string Default = "http://axschema.org/media/image/default";
+
+ /// <summary>Image (web URL) with equal width and height</summary>
+ /// <example>"http://example.com/~jsmith/image.jpg"</example>
+ public const string Aspect11 = "http://axschema.org/media/image/aspect11";
+
+ /// <summary>Image (web URL) 4:3 aspect ratio - landscape</summary>
+ /// <example>"http://example.com/~jsmith/image.jpg"</example>
+ public const string Aspect43 = "http://axschema.org/media/image/aspect43";
+
+ /// <summary>Image (web URL) 4:3 aspect ratio - landscape</summary>
+ /// <example>"http://example.com/~jsmith/image.jpg"</example>
+ public const string Aspect34 = "http://axschema.org/media/image/aspect34";
+
+ /// <summary>Image (web URL); favicon format as per FAVICON-W3C. The format for the image must be 16x16 pixels or 32x32 pixels, using either 8-bit or 24-bit colors. The format of the image must be one of PNG (a W3C standard), GIF, or ICO.</summary>
+ /// <example>"http://example.com/~jsmith/image.jpg"</example>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Fav", Justification = "By design")]
+ public const string FavIcon = "http://axschema.org/media/image/favicon";
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs
new file mode 100644
index 0000000..328d81f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs
@@ -0,0 +1,223 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExtensionArgumentsManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Manages the processing and construction of OpenID extensions parts.
+ /// </summary>
+ internal class ExtensionArgumentsManager {
+ /// <summary>
+ /// This contains a set of aliases that we must be willing to implicitly
+ /// match to namespaces for backward compatibility with other OpenID libraries.
+ /// </summary>
+ private static readonly Dictionary<string, string> typeUriToAliasAffinity = new Dictionary<string, string> {
+ { Extensions.SimpleRegistration.Constants.sreg_ns, Extensions.SimpleRegistration.Constants.sreg_compatibility_alias },
+ { Extensions.ProviderAuthenticationPolicy.Constants.TypeUri, Extensions.ProviderAuthenticationPolicy.Constants.CompatibilityAlias },
+ };
+
+ /// <summary>
+ /// The version of OpenID that the message is using.
+ /// </summary>
+ private Protocol protocol;
+
+ /// <summary>
+ /// Whether extensions are being read or written.
+ /// </summary>
+ private bool isReadMode;
+
+ /// <summary>
+ /// The alias manager that will track Type URI to alias mappings.
+ /// </summary>
+ private AliasManager aliasManager = new AliasManager();
+
+ /// <summary>
+ /// A complex dictionary where the key is the Type URI of the extension,
+ /// and the value is another dictionary of the name/value args of the extension.
+ /// </summary>
+ private Dictionary<string, IDictionary<string, string>> extensions = new Dictionary<string, IDictionary<string, string>>();
+
+ /// <summary>
+ /// Prevents a default instance of the <see cref="ExtensionArgumentsManager"/> class from being created.
+ /// </summary>
+ private ExtensionArgumentsManager() { }
+
+ /// <summary>
+ /// Gets a value indicating whether the extensions are being read (as opposed to written).
+ /// </summary>
+ internal bool ReadMode {
+ get { return this.isReadMode; }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="ExtensionArgumentsManager"/> instance to process incoming extensions.
+ /// </summary>
+ /// <param name="query">The parameters in the OpenID message.</param>
+ /// <returns>The newly created instance of <see cref="ExtensionArgumentsManager"/>.</returns>
+ public static ExtensionArgumentsManager CreateIncomingExtensions(IDictionary<string, string> query) {
+ Requires.NotNull(query, "query");
+ var mgr = new ExtensionArgumentsManager();
+ mgr.protocol = Protocol.Detect(query);
+ mgr.isReadMode = true;
+ string aliasPrefix = mgr.protocol.openid.ns + ".";
+
+ // First pass looks for namespace aliases
+ foreach (var pair in query) {
+ if (pair.Key.StartsWith(aliasPrefix, StringComparison.Ordinal)) {
+ mgr.aliasManager.SetAlias(pair.Key.Substring(aliasPrefix.Length), pair.Value);
+ }
+ }
+
+ // For backwards compatibility, add certain aliases if they aren't defined.
+ if (mgr.protocol.Version.Major < 2) {
+ foreach (var pair in typeUriToAliasAffinity) {
+ if (!mgr.aliasManager.IsAliasAssignedTo(pair.Key) &&
+ !mgr.aliasManager.IsAliasUsed(pair.Value)) {
+ mgr.aliasManager.SetAlias(pair.Value, pair.Key);
+ }
+ }
+ }
+
+ // Second pass looks for extensions using those aliases
+ foreach (var pair in query) {
+ if (!pair.Key.StartsWith(mgr.protocol.openid.Prefix, StringComparison.Ordinal)) {
+ continue;
+ }
+ string possibleAlias = pair.Key.Substring(mgr.protocol.openid.Prefix.Length);
+ int periodIndex = possibleAlias.IndexOf(".", StringComparison.Ordinal);
+ if (periodIndex >= 0) {
+ possibleAlias = possibleAlias.Substring(0, periodIndex);
+ }
+ string typeUri;
+ if ((typeUri = mgr.aliasManager.TryResolveAlias(possibleAlias)) != null) {
+ if (!mgr.extensions.ContainsKey(typeUri)) {
+ mgr.extensions[typeUri] = new Dictionary<string, string>();
+ }
+ string key = periodIndex >= 0 ? pair.Key.Substring(mgr.protocol.openid.Prefix.Length + possibleAlias.Length + 1) : string.Empty;
+ mgr.extensions[typeUri].Add(key, pair.Value);
+ }
+ }
+ return mgr;
+ }
+
+ /// <summary>
+ /// Creates a <see cref="ExtensionArgumentsManager"/> instance to prepare outgoing extensions.
+ /// </summary>
+ /// <param name="protocol">The protocol version used for the outgoing message.</param>
+ /// <returns>
+ /// The newly created instance of <see cref="ExtensionArgumentsManager"/>.
+ /// </returns>
+ public static ExtensionArgumentsManager CreateOutgoingExtensions(Protocol protocol) {
+ var mgr = new ExtensionArgumentsManager();
+ mgr.protocol = protocol;
+
+ // Affinity for certain alias for backwards compatibility
+ foreach (var pair in typeUriToAliasAffinity) {
+ mgr.aliasManager.SetAlias(pair.Value, pair.Key);
+ }
+ return mgr;
+ }
+
+ /// <summary>
+ /// Adds query parameters for OpenID extensions to the request directed
+ /// at the OpenID provider.
+ /// </summary>
+ /// <param name="extensionTypeUri">The extension type URI.</param>
+ /// <param name="arguments">The arguments for this extension to add to the message.</param>
+ public void AddExtensionArguments(string extensionTypeUri, IDictionary<string, string> arguments) {
+ Requires.ValidState(!this.ReadMode);
+ Requires.NotNullOrEmpty(extensionTypeUri, "extensionTypeUri");
+ Requires.NotNull(arguments, "arguments");
+ if (arguments.Count == 0) {
+ return;
+ }
+
+ IDictionary<string, string> extensionArgs;
+ if (!this.extensions.TryGetValue(extensionTypeUri, out extensionArgs)) {
+ this.extensions.Add(extensionTypeUri, extensionArgs = new Dictionary<string, string>(arguments.Count));
+ }
+
+ ErrorUtilities.VerifyProtocol(extensionArgs.Count == 0, OpenIdStrings.ExtensionAlreadyAddedWithSameTypeURI, extensionTypeUri);
+ foreach (var pair in arguments) {
+ extensionArgs.Add(pair.Key, pair.Value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the actual arguments to add to a querystring or other response,
+ /// where type URI, alias, and actual key/values are all defined.
+ /// </summary>
+ /// <param name="includeOpenIdPrefix">
+ /// <c>true</c> if the generated parameter names should include the 'openid.' prefix.
+ /// This should be <c>true</c> for all but direct response messages.
+ /// </param>
+ /// <returns>A dictionary of key=value pairs to add to the message to carry the extension.</returns>
+ internal IDictionary<string, string> GetArgumentsToSend(bool includeOpenIdPrefix) {
+ Requires.ValidState(!this.ReadMode);
+ Dictionary<string, string> args = new Dictionary<string, string>();
+ foreach (var typeUriAndExtension in this.extensions) {
+ string typeUri = typeUriAndExtension.Key;
+ var extensionArgs = typeUriAndExtension.Value;
+ if (extensionArgs.Count == 0) {
+ continue;
+ }
+ string alias = this.aliasManager.GetAlias(typeUri);
+
+ // send out the alias declaration
+ string openidPrefix = includeOpenIdPrefix ? this.protocol.openid.Prefix : string.Empty;
+ args.Add(openidPrefix + this.protocol.openidnp.ns + "." + alias, typeUri);
+ string prefix = openidPrefix + alias;
+ foreach (var pair in extensionArgs) {
+ string key = prefix;
+ if (pair.Key.Length > 0) {
+ key += "." + pair.Key;
+ }
+ args.Add(key, pair.Value);
+ }
+ }
+ return args;
+ }
+
+ /// <summary>
+ /// Gets the fields carried by a given OpenId extension.
+ /// </summary>
+ /// <param name="extensionTypeUri">The type URI of the extension whose fields are being queried for.</param>
+ /// <returns>
+ /// The fields included in the given extension, or null if the extension is not present.
+ /// </returns>
+ internal IDictionary<string, string> GetExtensionArguments(string extensionTypeUri) {
+ Requires.NotNullOrEmpty(extensionTypeUri, "extensionTypeUri");
+ Requires.ValidState(this.ReadMode);
+
+ IDictionary<string, string> extensionArgs;
+ this.extensions.TryGetValue(extensionTypeUri, out extensionArgs);
+ return extensionArgs;
+ }
+
+ /// <summary>
+ /// Gets whether any arguments for a given extension are present.
+ /// </summary>
+ /// <param name="extensionTypeUri">The extension Type URI in question.</param>
+ /// <returns><c>true</c> if this extension is present; <c>false</c> otherwise.</returns>
+ internal bool ContainsExtension(string extensionTypeUri) {
+ return this.extensions.ContainsKey(extensionTypeUri);
+ }
+
+ /// <summary>
+ /// Gets the type URIs of all discovered extensions in the message.
+ /// </summary>
+ /// <returns>A sequence of the type URIs.</returns>
+ internal IEnumerable<string> GetExtensionTypeUris() {
+ return this.extensions.Keys;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs
new file mode 100644
index 0000000..108ac52
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs
@@ -0,0 +1,190 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExtensionBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A handy base class for built-in extensions.
+ /// </summary>
+ [Serializable]
+ public class ExtensionBase : IOpenIdMessageExtension {
+ /// <summary>
+ /// Backing store for the <see cref="IOpenIdMessageExtension.TypeUri"/> property.
+ /// </summary>
+ private string typeUri;
+
+ /// <summary>
+ /// Backing store for the <see cref="IOpenIdMessageExtension.AdditionalSupportedTypeUris"/> property.
+ /// </summary>
+ private IEnumerable<string> additionalSupportedTypeUris;
+
+ /// <summary>
+ /// Backing store for the <see cref="IMessage.ExtraData"/> property.
+ /// </summary>
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExtensionBase"/> class.
+ /// </summary>
+ /// <param name="version">The version of the extension.</param>
+ /// <param name="typeUri">The type URI to use in the OpenID message.</param>
+ /// <param name="additionalSupportedTypeUris">The additional supported type URIs by which this extension might be recognized. May be null.</param>
+ protected ExtensionBase(Version version, string typeUri, IEnumerable<string> additionalSupportedTypeUris) {
+ this.Version = version;
+ this.typeUri = typeUri;
+ this.additionalSupportedTypeUris = additionalSupportedTypeUris ?? EmptyList<string>.Instance;
+ }
+
+ #region IOpenIdProtocolMessageExtension Members
+
+ /// <summary>
+ /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements.
+ /// </summary>
+ string IOpenIdMessageExtension.TypeUri {
+ get { return this.TypeUri; }
+ }
+
+ /// <summary>
+ /// Gets the additional TypeURIs that are supported by this extension, in preferred order.
+ /// May be empty if none other than <see cref="IOpenIdMessageExtension.TypeUri"/> is supported, but
+ /// should not be null.
+ /// </summary>
+ /// <remarks>
+ /// Useful for reading in messages with an older version of an extension.
+ /// The value in the <see cref="IOpenIdMessageExtension.TypeUri"/> property is always checked before
+ /// trying this list.
+ /// If you do support multiple versions of an extension using this method,
+ /// consider adding a CreateResponse method to your request extension class
+ /// so that the response can have the context it needs to remain compatible
+ /// given the version of the extension in the request message.
+ /// The <see cref="SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example.
+ /// </remarks>
+ IEnumerable<string> IOpenIdMessageExtension.AdditionalSupportedTypeUris {
+ get { return this.AdditionalSupportedTypeUris; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this extension was
+ /// signed by the OpenID Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the provider; otherwise, <c>false</c>.
+ /// </value>
+ bool IOpenIdMessageExtension.IsSignedByRemoteParty {
+ get { return this.IsSignedByRemoteParty; }
+ set { this.IsSignedByRemoteParty = value; }
+ }
+
+ #endregion
+
+ #region IMessage Properties
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ public Version Version { get; private set; }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ IDictionary<string, string> IMessage.ExtraData {
+ get { return this.ExtraData; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements.
+ /// </summary>
+ protected string TypeUri {
+ get { return this.typeUri; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this extension was
+ /// signed by the OpenID Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the provider; otherwise, <c>false</c>.
+ /// </value>
+ protected bool IsSignedByRemoteParty { get; set; }
+
+ /// <summary>
+ /// Gets the additional TypeURIs that are supported by this extension, in preferred order.
+ /// May be empty if none other than <see cref="IOpenIdMessageExtension.TypeUri"/> is supported, but
+ /// should not be null.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Useful for reading in messages with an older version of an extension.
+ /// The value in the <see cref="IOpenIdMessageExtension.TypeUri"/> property is always checked before
+ /// trying this list.
+ /// If you do support multiple versions of an extension using this method,
+ /// consider adding a CreateResponse method to your request extension class
+ /// so that the response can have the context it needs to remain compatible
+ /// given the version of the extension in the request message.
+ /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example.
+ /// </remarks>
+ protected IEnumerable<string> AdditionalSupportedTypeUris {
+ get { return this.additionalSupportedTypeUris; }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ protected IDictionary<string, string> ExtraData {
+ get { return this.extraData; }
+ }
+
+ #region IMessage Methods
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ void IMessage.EnsureValidMessage() {
+ this.EnsureValidMessage();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ protected virtual void EnsureValidMessage() {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs
new file mode 100644
index 0000000..b44f797
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------
+// <copyright file="IClientScriptExtensionResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// An interface that OpenID extensions can implement to allow authentication response
+ /// messages with included extensions to be processed by Javascript on the user agent.
+ /// </summary>
+ public interface IClientScriptExtensionResponse : IExtensionMessage {
+ /// <summary>
+ /// Reads the extension information on an authentication response from the provider.
+ /// </summary>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <returns>
+ /// A Javascript snippet that when executed on the user agent returns an object with
+ /// the information deserialized from the extension response.
+ /// </returns>
+ /// <remarks>
+ /// This method is called <b>before</b> the signature on the assertion response has been
+ /// verified. Therefore all information in these fields should be assumed unreliable
+ /// and potentially falsified.
+ /// </remarks>
+ string InitializeJavaScriptData(IProtocolMessageWithExtensions response);
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs
new file mode 100644
index 0000000..5e7bc49
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthorizationApprovedResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.OAuth {
+ using System;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// The OAuth response that a Provider may include with a positive
+ /// OpenID identity assertion with an approved request token.
+ /// </summary>
+ [Serializable]
+ public class AuthorizationApprovedResponse : ExtensionBase {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && !isProviderRole && data.ContainsKey(Constants.RequestTokenParameter)) {
+ return new AuthorizationApprovedResponse();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthorizationApprovedResponse"/> class.
+ /// </summary>
+ public AuthorizationApprovedResponse()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+
+ /// <summary>
+ /// Gets or sets the user-approved request token.
+ /// </summary>
+ /// <value>The request token.</value>
+ [MessagePart(Constants.RequestTokenParameter, IsRequired = true, AllowEmpty = false)]
+ public string RequestToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets a string that encodes, in a way possibly specific to the Combined Provider, one or more scopes that the returned request token is valid for. This will typically indicate a subset of the scopes requested in Section 8.
+ /// </summary>
+ [MessagePart("scope", IsRequired = false, AllowEmpty = true)]
+ public string Scope { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs
new file mode 100644
index 0000000..7c3a5ad
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthorizationDeclinedResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.OAuth {
+ using System;
+
+ /// <summary>
+ /// The OAuth response that a Provider should include with a positive
+ /// OpenID identity assertion when OAuth authorization was declined.
+ /// </summary>
+ [Serializable]
+ public class AuthorizationDeclinedResponse : ExtensionBase {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && !isProviderRole && !data.ContainsKey(Constants.RequestTokenParameter)) {
+ return new AuthorizationDeclinedResponse();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthorizationDeclinedResponse"/> class.
+ /// </summary>
+ public AuthorizationDeclinedResponse()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs
new file mode 100644
index 0000000..99f0880
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthorizationRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.OAuth {
+ using System;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An extension to include with an authentication request in order to also
+ /// obtain authorization to access user data at the combined OpenID Provider
+ /// and Service Provider.
+ /// </summary>
+ /// <remarks>
+ /// <para>When requesting OpenID Authentication via the protocol mode "checkid_setup"
+ /// or "checkid_immediate", this extension can be used to request that the end
+ /// user authorize an OAuth access token at the same time as an OpenID
+ /// authentication. This is done by sending the following parameters as part
+ /// of the OpenID request. (Note that the use of "oauth" as part of the parameter
+ /// names here and in subsequent sections is just an example. See Section 5 for details.)</para>
+ /// <para>See section 8.</para>
+ /// </remarks>
+ [Serializable]
+ public class AuthorizationRequest : ExtensionBase {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && isProviderRole) {
+ return new AuthorizationRequest();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthorizationRequest"/> class.
+ /// </summary>
+ public AuthorizationRequest()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ }
+
+ /// <summary>
+ /// Gets or sets the consumer key agreed upon between the Consumer and Service Provider.
+ /// </summary>
+ [MessagePart("consumer", IsRequired = true, AllowEmpty = false)]
+ public string Consumer { get; set; }
+
+ /// <summary>
+ /// Gets or sets a string that encodes, in a way possibly specific to the Combined Provider, one or more scopes for the OAuth token expected in the authentication response.
+ /// </summary>
+ [MessagePart("scope", IsRequired = false)]
+ public string Scope { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs
new file mode 100644
index 0000000..32efee9
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs
@@ -0,0 +1,22 @@
+//-----------------------------------------------------------------------
+// <copyright file="Constants.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.OAuth {
+ /// <summary>
+ /// Constants used in the OpenID OAuth extension.
+ /// </summary>
+ internal static class Constants {
+ /// <summary>
+ /// The TypeURI for the OpenID OAuth extension.
+ /// </summary>
+ internal const string TypeUri = "http://specs.openid.net/extensions/oauth/1.0";
+
+ /// <summary>
+ /// The name of the parameter that carries the request token in the response.
+ /// </summary>
+ internal const string RequestTokenParameter = "request_token";
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs
new file mode 100644
index 0000000..95dd2c9
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdExtensionFactoryAggregator.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// An OpenID extension factory that only delegates extension
+ /// instantiation requests to other factories.
+ /// </summary>
+ internal class OpenIdExtensionFactoryAggregator : IOpenIdExtensionFactory {
+ /// <summary>
+ /// The list of factories this factory delegates to.
+ /// </summary>
+ private List<IOpenIdExtensionFactory> factories = new List<IOpenIdExtensionFactory>(2);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdExtensionFactoryAggregator"/> class.
+ /// </summary>
+ internal OpenIdExtensionFactoryAggregator() {
+ }
+
+ /// <summary>
+ /// Gets the extension factories that this aggregating factory delegates to.
+ /// </summary>
+ /// <value>A list of factories. May be empty, but never null.</value>
+ internal IList<IOpenIdExtensionFactory> Factories {
+ get { return this.factories; }
+ }
+
+ #region IOpenIdExtensionFactory Members
+
+ /// <summary>
+ /// Creates a new instance of some extension based on the received extension parameters.
+ /// </summary>
+ /// <param name="typeUri">The type URI of the extension.</param>
+ /// <param name="data">The parameters associated specifically with this extension.</param>
+ /// <param name="baseMessage">The OpenID message carrying this extension.</param>
+ /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param>
+ /// <returns>
+ /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes
+ /// the extension described in the input parameters; <c>null</c> otherwise.
+ /// </returns>
+ /// <remarks>
+ /// This factory method need only initialize properties in the instantiated extension object
+ /// that are not bound using <see cref="MessagePartAttribute"/>.
+ /// </remarks>
+ public IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) {
+ foreach (var factory in this.factories) {
+ IOpenIdMessageExtension result = factory.Create(typeUri, data, baseMessage, isProviderRole);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Loads the default factory and additional ones given by the configuration.
+ /// </summary>
+ /// <returns>A new instance of <see cref="OpenIdExtensionFactoryAggregator"/>.</returns>
+ internal static OpenIdExtensionFactoryAggregator LoadFromConfiguration() {
+ Contract.Ensures(Contract.Result<OpenIdExtensionFactoryAggregator>() != null);
+ var factoriesElement = DotNetOpenAuth.Configuration.OpenIdElement.Configuration.ExtensionFactories;
+ var aggregator = new OpenIdExtensionFactoryAggregator();
+ aggregator.Factories.Add(new StandardOpenIdExtensionFactory());
+ aggregator.factories.AddRange(factoriesElement.CreateInstances(false));
+ return aggregator;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionsInteropHelper.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionsInteropHelper.cs
new file mode 100644
index 0000000..fb6202e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionsInteropHelper.cs
@@ -0,0 +1,124 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdExtensionsInteropHelper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A set of methods designed to assist in improving interop across different
+ /// OpenID implementations and their extensions.
+ /// </summary>
+ internal static class OpenIdExtensionsInteropHelper {
+ /// <summary>
+ /// The gender decoder to translate AX genders to Sreg.
+ /// </summary>
+ private static GenderEncoder genderEncoder = new GenderEncoder();
+
+ /// <summary>
+ /// Gets the gender decoder to translate AX genders to Sreg.
+ /// </summary>
+ internal static GenderEncoder GenderEncoder {
+ get { return genderEncoder; }
+ }
+
+ /// <summary>
+ /// Splits the AX attribute format flags into individual values for processing.
+ /// </summary>
+ /// <param name="formats">The formats to split up into individual flags.</param>
+ /// <returns>A sequence of individual flags.</returns>
+ internal static IEnumerable<AXAttributeFormats> ForEachFormat(AXAttributeFormats formats) {
+ if ((formats & AXAttributeFormats.AXSchemaOrg) != 0) {
+ yield return AXAttributeFormats.AXSchemaOrg;
+ }
+
+ if ((formats & AXAttributeFormats.OpenIdNetSchema) != 0) {
+ yield return AXAttributeFormats.OpenIdNetSchema;
+ }
+
+ if ((formats & AXAttributeFormats.SchemaOpenIdNet) != 0) {
+ yield return AXAttributeFormats.SchemaOpenIdNet;
+ }
+ }
+
+ /// <summary>
+ /// Transforms an AX attribute type URI from the axschema.org format into a given format.
+ /// </summary>
+ /// <param name="axSchemaOrgFormatTypeUri">The ax schema org format type URI.</param>
+ /// <param name="targetFormat">The target format. Only one flag should be set.</param>
+ /// <returns>The AX attribute type URI in the target format.</returns>
+ internal static string TransformAXFormat(string axSchemaOrgFormatTypeUri, AXAttributeFormats targetFormat) {
+ Requires.NotNullOrEmpty(axSchemaOrgFormatTypeUri, "axSchemaOrgFormatTypeUri");
+
+ switch (targetFormat) {
+ case AXAttributeFormats.AXSchemaOrg:
+ return axSchemaOrgFormatTypeUri;
+ case AXAttributeFormats.SchemaOpenIdNet:
+ return axSchemaOrgFormatTypeUri.Replace("axschema.org", "schema.openid.net");
+ case AXAttributeFormats.OpenIdNetSchema:
+ return axSchemaOrgFormatTypeUri.Replace("axschema.org", "openid.net/schema");
+ default:
+ throw new ArgumentOutOfRangeException("targetFormat");
+ }
+ }
+
+ /// <summary>
+ /// Detects the AX attribute type URI format from a given sample.
+ /// </summary>
+ /// <param name="typeURIs">The type URIs to scan for recognized formats.</param>
+ /// <returns>The first AX type URI format recognized in the list.</returns>
+ internal static AXAttributeFormats DetectAXFormat(IEnumerable<string> typeURIs) {
+ Requires.NotNull(typeURIs, "typeURIs");
+
+ if (typeURIs.Any(uri => uri.StartsWith("http://axschema.org/", StringComparison.Ordinal))) {
+ return AXAttributeFormats.AXSchemaOrg;
+ }
+
+ if (typeURIs.Any(uri => uri.StartsWith("http://schema.openid.net/", StringComparison.Ordinal))) {
+ return AXAttributeFormats.SchemaOpenIdNet;
+ }
+
+ if (typeURIs.Any(uri => uri.StartsWith("http://openid.net/schema/", StringComparison.Ordinal))) {
+ return AXAttributeFormats.OpenIdNetSchema;
+ }
+
+ return AXAttributeFormats.None;
+ }
+
+ /// <summary>
+ /// Adds an attribute fetch request if it is not already present in the AX request.
+ /// </summary>
+ /// <param name="ax">The AX request to add the attribute request to.</param>
+ /// <param name="format">The format of the attribute's Type URI to use.</param>
+ /// <param name="axSchemaOrgFormatAttribute">The attribute in axschema.org format.</param>
+ /// <param name="demandLevel">The demand level.</param>
+ internal static void FetchAttribute(FetchRequest ax, AXAttributeFormats format, string axSchemaOrgFormatAttribute, DemandLevel demandLevel) {
+ Requires.NotNull(ax, "ax");
+ Requires.NotNullOrEmpty(axSchemaOrgFormatAttribute, "axSchemaOrgFormatAttribute");
+
+ string typeUri = TransformAXFormat(axSchemaOrgFormatAttribute, format);
+ if (!ax.Attributes.Contains(typeUri)) {
+ switch (demandLevel) {
+ case DemandLevel.Request:
+ ax.Attributes.AddOptional(typeUri);
+ break;
+ case DemandLevel.Require:
+ ax.Attributes.AddRequired(typeUri);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs
new file mode 100644
index 0000000..99c7a2e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthenticationPolicies.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// Well-known authentication policies defined in the PAPE extension spec or by a recognized
+ /// standards body.
+ /// </summary>
+ /// <remarks>
+ /// This is a class of constants rather than a flags enum because policies may be
+ /// freely defined and used by anyone, just by using a new Uri.
+ /// </remarks>
+ public static class AuthenticationPolicies {
+ /// <summary>
+ /// An authentication mechanism where the End User does not provide a shared secret to a party potentially under the control of the Relying Party. (Note that the potentially malicious Relying Party controls where the User-Agent is redirected to and thus may not send it to the End User's actual OpenID Provider).
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Phishing", Justification = "By design")]
+ public const string PhishingResistant = "http://schemas.openid.net/pape/policies/2007/06/phishing-resistant";
+
+ /// <summary>
+ /// An authentication mechanism where the End User authenticates to the OpenID Provider by providing over one authentication factor. Common authentication factors are something you know, something you have, and something you are. An example would be authentication using a password and a software token or digital certificate.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "By design")]
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "MultiFactor", Justification = "By design")]
+ public const string MultiFactor = "http://schemas.openid.net/pape/policies/2007/06/multi-factor";
+
+ /// <summary>
+ /// An authentication mechanism where the End User authenticates to the OpenID Provider by providing over one authentication factor where at least one of the factors is a physical factor such as a hardware device or biometric. Common authentication factors are something you know, something you have, and something you are. This policy also implies the Multi-Factor Authentication policy (http://schemas.openid.net/pape/policies/2007/06/multi-factor) and both policies MAY BE specified in conjunction without conflict. An example would be authentication using a password and a hardware token.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "MultiFactor", Justification = "By design")]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "By design")]
+ public const string PhysicalMultiFactor = "http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical";
+
+ /// <summary>
+ /// Indicates that the Provider MUST use a pair-wise pseudonym for the user that is persistent
+ /// and unique across the requesting realm as the openid.claimed_id and openid.identity (see Section 4.2).
+ /// </summary>
+ public const string PrivatePersonalIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier";
+
+ /// <summary>
+ /// Indicates that the OP MUST only respond with a positive assertion if the requirements demonstrated
+ /// by the OP to obtain certification by a Federally adopted Trust Framework Provider have been met.
+ /// </summary>
+ /// <remarks>
+ /// Notwithstanding the RP may request this authentication policy, the RP MUST still
+ /// verify that this policy appears in the positive assertion response rather than assume the OP
+ /// recognized and complied with the request.
+ /// </remarks>
+ public const string USGovernmentTrustLevel1 = "http://www.idmanagement.gov/schema/2009/05/icam/openid-trust-level1.pdf";
+
+ /// <summary>
+ /// Indicates that the OP MUST not include any OpenID Attribute Exchange or Simple Registration
+ /// information regarding the user in the assertion.
+ /// </summary>
+ public const string NoPersonallyIdentifiableInformation = "http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf";
+
+ /// <summary>
+ /// Used in a PAPE response to indicate that no PAPE authentication policies could be satisfied.
+ /// </summary>
+ /// <remarks>
+ /// Used internally by the PAPE extension, so that users don't have to know about it.
+ /// </remarks>
+ internal const string None = "http://schemas.openid.net/pape/policies/2007/06/none";
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs
new file mode 100644
index 0000000..93e76d5
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs
@@ -0,0 +1,77 @@
+//-----------------------------------------------------------------------
+// <copyright file="Constants.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ /// <summary>
+ /// OpenID Provider Authentication Policy extension constants.
+ /// </summary>
+ internal static class Constants {
+ /// <summary>
+ /// The namespace used by this extension in messages.
+ /// </summary>
+ internal const string TypeUri = "http://specs.openid.net/extensions/pape/1.0";
+
+ /// <summary>
+ /// The namespace alias to use for OpenID 1.x interop, where aliases are not defined in the message.
+ /// </summary>
+ internal const string CompatibilityAlias = "pape";
+
+ /// <summary>
+ /// The string to prepend on an Auth Level Type alias definition.
+ /// </summary>
+ internal const string AuthLevelNamespaceDeclarationPrefix = "auth_level.ns.";
+
+ /// <summary>
+ /// Well-known assurance level Type URIs.
+ /// </summary>
+ internal static class AssuranceLevels {
+ /// <summary>
+ /// A mapping between the PAPE TypeURI and the alias to use if
+ /// possible for backward compatibility reasons.
+ /// </summary>
+ internal static readonly IDictionary<string, string> PreferredTypeUriToAliasMap = new Dictionary<string, string> {
+ { NistTypeUri, "nist" },
+ };
+
+ /// <summary>
+ /// The Type URI of the NIST assurance level.
+ /// </summary>
+ internal const string NistTypeUri = "http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf";
+ }
+
+ /// <summary>
+ /// Parameters to be included with PAPE requests.
+ /// </summary>
+ internal static class RequestParameters {
+ /// <summary>
+ /// Optional. If the End User has not actively authenticated to the OP within the number of seconds specified in a manner fitting the requested policies, the OP SHOULD authenticate the End User for this request.
+ /// </summary>
+ /// <value>Integer value greater than or equal to zero in seconds.</value>
+ /// <remarks>
+ /// The OP should realize that not adhering to the request for re-authentication most likely means that the End User will not be allowed access to the services provided by the RP. If this parameter is absent in the request, the OP should authenticate the user at its own discretion.
+ /// </remarks>
+ internal const string MaxAuthAge = "max_auth_age";
+
+ /// <summary>
+ /// Zero or more authentication policy URIs that the OP SHOULD conform to when authenticating the user. If multiple policies are requested, the OP SHOULD satisfy as many as it can.
+ /// </summary>
+ /// <value>Space separated list of authentication policy URIs.</value>
+ /// <remarks>
+ /// If no policies are requested, the RP may be interested in other information such as the authentication age.
+ /// </remarks>
+ internal const string PreferredAuthPolicies = "preferred_auth_policies";
+
+ /// <summary>
+ /// The space separated list of the name spaces of the custom Assurance Level that RP requests, in the order of its preference.
+ /// </summary>
+ internal const string PreferredAuthLevelTypes = "preferred_auth_level_types";
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs
new file mode 100644
index 0000000..9dc0574
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs
@@ -0,0 +1,69 @@
+//-----------------------------------------------------------------------
+// <copyright file="DateTimeEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// An encoder/decoder design for DateTimes that must conform to the PAPE spec.
+ /// </summary>
+ /// <remarks>
+ /// The timestamp MUST be formatted as specified in section 5.6 of [RFC3339] (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), with the following restrictions:
+ /// * All times must be in the UTC timezone, indicated with a "Z".
+ /// * No fractional seconds are allowed
+ /// For example: 2005-05-15T17:11:51Z
+ /// </remarks>
+ internal class DateTimeEncoder : IMessagePartEncoder {
+ /// <summary>
+ /// An array of the date/time formats allowed by the PAPE extension.
+ /// </summary>
+ /// <remarks>
+ /// TODO: This array of formats is not yet a complete list.
+ /// </remarks>
+ private static readonly string[] PermissibleDateTimeFormats = { "yyyy-MM-ddTHH:mm:ssZ" };
+
+ #region IMessagePartEncoder Members
+
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>
+ /// The <paramref name="value"/> in string form, ready for message transport.
+ /// </returns>
+ public string Encode(object value) {
+ DateTime? dateTime = value as DateTime?;
+ if (dateTime.HasValue) {
+ return dateTime.Value.ToUniversalTimeSafe().ToString(PermissibleDateTimeFormats[0], CultureInfo.InvariantCulture);
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>
+ /// The deserialized form of the given string.
+ /// </returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ public object Decode(string value) {
+ DateTime dateTime;
+ if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dateTime) && dateTime.Kind == DateTimeKind.Utc) { // may be unspecified per our option above
+ return dateTime;
+ } else {
+ Logger.OpenId.ErrorFormat("Invalid format for message part: {0}", value);
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs
new file mode 100644
index 0000000..3031aad
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------
+// <copyright file="NistAssuranceLevel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Text;
+
+ /// <summary>
+ /// Descriptions for NIST-defined levels of assurance that a credential
+ /// has not been compromised and therefore the extent to which an
+ /// authentication assertion can be trusted.
+ /// </summary>
+ /// <remarks>
+ /// <para>One using this enum should review the following publication for details
+ /// before asserting or interpreting what these levels signify, notwithstanding
+ /// the brief summaries attached to each level in DotNetOpenAuth documentation.
+ /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf</para>
+ /// <para>
+ /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels.
+ /// </para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nist", Justification = "By design")]
+ public enum NistAssuranceLevel {
+ /// <summary>
+ /// Not an assurance level defined by NIST, but rather SHOULD be used to
+ /// signify that the OP recognizes the parameter and the End User
+ /// authentication did not meet the requirements of Level 1.
+ /// </summary>
+ InsufficientForLevel1 = 0,
+
+ /// <summary>
+ /// See this document for a thorough description:
+ /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf
+ /// </summary>
+ Level1 = 1,
+
+ /// <summary>
+ /// See this document for a thorough description:
+ /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf
+ /// </summary>
+ Level2 = 2,
+
+ /// <summary>
+ /// See this document for a thorough description:
+ /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf
+ /// </summary>
+ Level3 = 3,
+
+ /// <summary>
+ /// See this document for a thorough description:
+ /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf
+ /// </summary>
+ Level4 = 4,
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs
new file mode 100644
index 0000000..d8ffb63
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs
@@ -0,0 +1,65 @@
+//-----------------------------------------------------------------------
+// <copyright file="PapeUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Utility methods for use by the PAPE extension.
+ /// </summary>
+ internal static class PapeUtilities {
+ /// <summary>
+ /// Looks at the incoming fields and figures out what the aliases and name spaces for auth level types are.
+ /// </summary>
+ /// <param name="fields">The incoming message data in which to discover TypeURIs and aliases.</param>
+ /// <returns>The <see cref="AliasManager"/> initialized with the given data.</returns>
+ internal static AliasManager FindIncomingAliases(IDictionary<string, string> fields) {
+ AliasManager aliasManager = new AliasManager();
+
+ foreach (var pair in fields) {
+ if (!pair.Key.StartsWith(Constants.AuthLevelNamespaceDeclarationPrefix, StringComparison.Ordinal)) {
+ continue;
+ }
+
+ string alias = pair.Key.Substring(Constants.AuthLevelNamespaceDeclarationPrefix.Length);
+ aliasManager.SetAlias(alias, pair.Value);
+ }
+
+ aliasManager.SetPreferredAliasesWhereNotSet(Constants.AssuranceLevels.PreferredTypeUriToAliasMap);
+
+ return aliasManager;
+ }
+
+ /// <summary>
+ /// Concatenates a sequence of strings using a space as a separator.
+ /// </summary>
+ /// <param name="values">The elements to concatenate together..</param>
+ /// <returns>The concatenated string of elements.</returns>
+ /// <exception cref="FormatException">Thrown if any element in the sequence includes a space.</exception>
+ internal static string ConcatenateListOfElements(IEnumerable<string> values) {
+ Requires.NotNull(values, "values");
+
+ StringBuilder valuesList = new StringBuilder();
+ foreach (string value in values.Distinct()) {
+ if (value.Contains(" ")) {
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidUri, value));
+ }
+ valuesList.Append(value);
+ valuesList.Append(" ");
+ }
+ if (valuesList.Length > 0) {
+ valuesList.Length -= 1; // remove trailing space
+ }
+ return valuesList.ToString();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs
new file mode 100644
index 0000000..84589dc
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs
@@ -0,0 +1,222 @@
+//-----------------------------------------------------------------------
+// <copyright file="PolicyRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The PAPE request part of an OpenID Authentication request message.
+ /// </summary>
+ [Serializable]
+ public sealed class PolicyRequest : ExtensionBase, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && isProviderRole) {
+ return new PolicyRequest();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The transport field for the RP's preferred authentication policies.
+ /// </summary>
+ /// <remarks>
+ /// This field is written to/read from during custom serialization.
+ /// </remarks>
+ [MessagePart("preferred_auth_policies", IsRequired = true)]
+ private string preferredPoliciesString;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PolicyRequest"/> class.
+ /// </summary>
+ public PolicyRequest()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ this.PreferredPolicies = new List<string>(1);
+ this.PreferredAuthLevelTypes = new List<string>(1);
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum acceptable time since the End User has
+ /// actively authenticated to the OP in a manner fitting the requested
+ /// policies, beyond which the Provider SHOULD authenticate the
+ /// End User for this request.
+ /// </summary>
+ /// <remarks>
+ /// The OP should realize that not adhering to the request for re-authentication
+ /// most likely means that the End User will not be allowed access to the
+ /// services provided by the RP. If this parameter is absent in the request,
+ /// the OP should authenticate the user at its own discretion.
+ /// </remarks>
+ [MessagePart("max_auth_age", IsRequired = false, Encoder = typeof(TimespanSecondsEncoder))]
+ public TimeSpan? MaximumAuthenticationAge { get; set; }
+
+ /// <summary>
+ /// Gets the list of authentication policy URIs that the OP SHOULD
+ /// conform to when authenticating the user. If multiple policies are
+ /// requested, the OP SHOULD satisfy as many as it can.
+ /// </summary>
+ /// <value>List of authentication policy URIs obtainable from
+ /// the <see cref="AuthenticationPolicies"/> class or from a custom
+ /// list.</value>
+ /// <remarks>
+ /// If no policies are requested, the RP may be interested in other
+ /// information such as the authentication age.
+ /// </remarks>
+ public IList<string> PreferredPolicies { get; private set; }
+
+ /// <summary>
+ /// Gets the namespaces of the custom Assurance Level the
+ /// Relying Party requests, in the order of its preference.
+ /// </summary>
+ public IList<string> PreferredAuthLevelTypes { get; private set; }
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ var extraData = ((IMessage)this).ExtraData;
+ extraData.Clear();
+
+ this.preferredPoliciesString = SerializePolicies(this.PreferredPolicies);
+
+ if (this.PreferredAuthLevelTypes.Count > 0) {
+ AliasManager authLevelAliases = new AliasManager();
+ authLevelAliases.AssignAliases(this.PreferredAuthLevelTypes, Constants.AssuranceLevels.PreferredTypeUriToAliasMap);
+
+ // Add a definition for each Auth Level Type alias.
+ foreach (string alias in authLevelAliases.Aliases) {
+ extraData.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, authLevelAliases.ResolveAlias(alias));
+ }
+
+ // Now use the aliases for those type URIs to list a preferred order.
+ extraData.Add(Constants.RequestParameters.PreferredAuthLevelTypes, SerializeAuthLevels(this.PreferredAuthLevelTypes, authLevelAliases));
+ }
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ var extraData = ((IMessage)this).ExtraData;
+
+ this.PreferredPolicies.Clear();
+ string[] preferredPolicies = this.preferredPoliciesString.Split(' ');
+ foreach (string policy in preferredPolicies) {
+ if (policy.Length > 0) {
+ this.PreferredPolicies.Add(policy);
+ }
+ }
+
+ this.PreferredAuthLevelTypes.Clear();
+ AliasManager authLevelAliases = PapeUtilities.FindIncomingAliases(extraData);
+ string preferredAuthLevelAliases;
+ if (extraData.TryGetValue(Constants.RequestParameters.PreferredAuthLevelTypes, out preferredAuthLevelAliases)) {
+ foreach (string authLevelAlias in preferredAuthLevelAliases.Split(' ')) {
+ if (authLevelAlias.Length == 0) {
+ continue;
+ }
+ this.PreferredAuthLevelTypes.Add(authLevelAliases.ResolveAlias(authLevelAlias));
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ PolicyRequest other = obj as PolicyRequest;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.MaximumAuthenticationAge != other.MaximumAuthenticationAge) {
+ return false;
+ }
+
+ if (this.PreferredPolicies.Count != other.PreferredPolicies.Count) {
+ return false;
+ }
+
+ foreach (string policy in this.PreferredPolicies) {
+ if (!other.PreferredPolicies.Contains(policy)) {
+ return false;
+ }
+ }
+
+ if (this.PreferredAuthLevelTypes.Count != other.PreferredAuthLevelTypes.Count) {
+ return false;
+ }
+
+ foreach (string authLevel in this.PreferredAuthLevelTypes) {
+ if (!other.PreferredAuthLevelTypes.Contains(authLevel)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ // This is a poor hash function, but an site that cares will likely have a bunch
+ // of look-alike instances anyway, so a good hash function would still bunch
+ // all the instances into the same hash code.
+ if (this.MaximumAuthenticationAge.HasValue) {
+ return this.MaximumAuthenticationAge.Value.GetHashCode();
+ } else {
+ return 1;
+ }
+ }
+
+ /// <summary>
+ /// Serializes the policies as a single string per the PAPE spec..
+ /// </summary>
+ /// <param name="policies">The policies to include in the list.</param>
+ /// <returns>The concatenated string of the given policies.</returns>
+ private static string SerializePolicies(IEnumerable<string> policies) {
+ return PapeUtilities.ConcatenateListOfElements(policies);
+ }
+
+ /// <summary>
+ /// Serializes the auth levels to a list of aliases.
+ /// </summary>
+ /// <param name="preferredAuthLevelTypes">The preferred auth level types.</param>
+ /// <param name="aliases">The alias manager.</param>
+ /// <returns>A space-delimited list of aliases.</returns>
+ private static string SerializeAuthLevels(IList<string> preferredAuthLevelTypes, AliasManager aliases) {
+ var aliasList = new List<string>();
+ foreach (string typeUri in preferredAuthLevelTypes) {
+ aliasList.Add(aliases.GetAlias(typeUri));
+ }
+
+ return PapeUtilities.ConcatenateListOfElements(aliasList);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
new file mode 100644
index 0000000..1fddc22
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
@@ -0,0 +1,282 @@
+//-----------------------------------------------------------------------
+// <copyright file="PolicyResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The PAPE response part of an OpenID Authentication response message.
+ /// </summary>
+ [Serializable]
+ public sealed class PolicyResponse : ExtensionBase, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.TypeUri && !isProviderRole) {
+ return new PolicyResponse();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The first part of a parameter name that gives the custom string value for
+ /// the assurance level. The second part of the parameter name is the alias for
+ /// that assurance level.
+ /// </summary>
+ private const string AuthLevelAliasPrefix = "auth_level.";
+
+ /// <summary>
+ /// One or more authentication policy URIs that the OP conformed to when authenticating the End User.
+ /// </summary>
+ /// <value>Space separated list of authentication policy URIs.</value>
+ /// <remarks>
+ /// If no policies were met though the OP wishes to convey other information in the response, this parameter MUST be included with the value of "none".
+ /// </remarks>
+ [MessagePart("auth_policies", IsRequired = true)]
+ private string actualPoliciesString;
+
+ /// <summary>
+ /// Backing field for the <see cref="AuthenticationTimeUtc"/> property.
+ /// </summary>
+ private DateTime? authenticationTimeUtc;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PolicyResponse"/> class.
+ /// </summary>
+ public PolicyResponse()
+ : base(new Version(1, 0), Constants.TypeUri, null) {
+ this.ActualPolicies = new List<string>(1);
+ this.AssuranceLevels = new Dictionary<string, string>(1);
+ }
+
+ /// <summary>
+ /// Gets a list of authentication policy URIs that the
+ /// OP conformed to when authenticating the End User.
+ /// </summary>
+ public IList<string> ActualPolicies { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the most recent timestamp when the End User has
+ /// actively authenticated to the OP in a manner fitting the asserted policies.
+ /// </summary>
+ /// <remarks>
+ /// If the RP's request included the "openid.max_auth_age" parameter
+ /// then the OP MUST include "openid.auth_time" in its response.
+ /// If "openid.max_auth_age" was not requested, the OP MAY choose to include
+ /// "openid.auth_time" in its response.
+ /// </remarks>
+ [MessagePart("auth_time", Encoder = typeof(DateTimeEncoder))]
+ public DateTime? AuthenticationTimeUtc {
+ get {
+ return this.authenticationTimeUtc;
+ }
+
+ set {
+ Requires.True(!value.HasValue || value.Value.Kind != DateTimeKind.Unspecified, "value", OpenIdStrings.UnspecifiedDateTimeKindNotAllowed);
+
+ // Make sure that whatever is set here, it becomes UTC time.
+ if (value.HasValue) {
+ // Convert to UTC and cut to the second, since the protocol only allows for
+ // that level of precision.
+ this.authenticationTimeUtc = OpenIdUtilities.CutToSecond(value.Value.ToUniversalTimeSafe());
+ } else {
+ this.authenticationTimeUtc = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the Assurance Level as defined by the National
+ /// Institute of Standards and Technology (NIST) in Special Publication
+ /// 800-63 (Burr, W., Dodson, D., and W. Polk, Ed., “Electronic
+ /// Authentication Guideline,” April 2006.) [NIST_SP800‑63] corresponding
+ /// to the authentication method and policies employed by the OP when
+ /// authenticating the End User.
+ /// </summary>
+ /// <remarks>
+ /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level
+ /// example classifications of authentication methods within the defined
+ /// levels.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nist", Justification = "Acronym")]
+ public NistAssuranceLevel? NistAssuranceLevel {
+ get {
+ string levelString;
+ if (this.AssuranceLevels.TryGetValue(Constants.AssuranceLevels.NistTypeUri, out levelString)) {
+ return (NistAssuranceLevel)Enum.Parse(typeof(NistAssuranceLevel), levelString);
+ } else {
+ return null;
+ }
+ }
+
+ set {
+ if (value != null) {
+ this.AssuranceLevels[Constants.AssuranceLevels.NistTypeUri] = ((int)value).ToString(CultureInfo.InvariantCulture);
+ } else {
+ this.AssuranceLevels.Remove(Constants.AssuranceLevels.NistTypeUri);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a dictionary where keys are the authentication level type URIs and
+ /// the values are the per authentication level defined custom value.
+ /// </summary>
+ /// <remarks>
+ /// A very common key is <see cref="Constants.AssuranceLevels.NistTypeUri"/>
+ /// and values for this key are available in <see cref="NistAssuranceLevel"/>.
+ /// </remarks>
+ public IDictionary<string, string> AssuranceLevels { get; private set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this extension is signed by the Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsSignedByProvider {
+ get { return this.IsSignedByRemoteParty; }
+ }
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ var extraData = ((IMessage)this).ExtraData;
+ extraData.Clear();
+
+ this.actualPoliciesString = SerializePolicies(this.ActualPolicies);
+
+ if (this.AssuranceLevels.Count > 0) {
+ AliasManager aliases = new AliasManager();
+ aliases.AssignAliases(this.AssuranceLevels.Keys, Constants.AssuranceLevels.PreferredTypeUriToAliasMap);
+
+ // Add a definition for each Auth Level Type alias.
+ foreach (string alias in aliases.Aliases) {
+ extraData.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, aliases.ResolveAlias(alias));
+ }
+
+ // Now use the aliases for those type URIs to list the individual values.
+ foreach (var pair in this.AssuranceLevels) {
+ extraData.Add(AuthLevelAliasPrefix + aliases.GetAlias(pair.Key), pair.Value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ var extraData = ((IMessage)this).ExtraData;
+
+ this.ActualPolicies.Clear();
+ string[] actualPolicies = this.actualPoliciesString.Split(' ');
+ foreach (string policy in actualPolicies) {
+ if (policy.Length > 0 && policy != AuthenticationPolicies.None) {
+ this.ActualPolicies.Add(policy);
+ }
+ }
+
+ this.AssuranceLevels.Clear();
+ AliasManager authLevelAliases = PapeUtilities.FindIncomingAliases(extraData);
+ foreach (string authLevelAlias in authLevelAliases.Aliases) {
+ string authValue;
+ if (extraData.TryGetValue(AuthLevelAliasPrefix + authLevelAlias, out authValue)) {
+ string authLevelType = authLevelAliases.ResolveAlias(authLevelAlias);
+ this.AssuranceLevels[authLevelType] = authValue;
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ PolicyResponse other = obj as PolicyResponse;
+ if (other == null) {
+ return false;
+ }
+
+ if (this.AuthenticationTimeUtc != other.AuthenticationTimeUtc) {
+ return false;
+ }
+
+ if (this.AssuranceLevels.Count != other.AssuranceLevels.Count) {
+ return false;
+ }
+
+ foreach (var pair in this.AssuranceLevels) {
+ if (!other.AssuranceLevels.Contains(pair)) {
+ return false;
+ }
+ }
+
+ if (this.ActualPolicies.Count != other.ActualPolicies.Count) {
+ return false;
+ }
+
+ foreach (string policy in this.ActualPolicies) {
+ if (!other.ActualPolicies.Contains(policy)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ // This is a poor hash function, but an site that cares will likely have a bunch
+ // of look-alike instances anyway, so a good hash function would still bunch
+ // all the instances into the same hash code.
+ if (this.AuthenticationTimeUtc.HasValue) {
+ return this.AuthenticationTimeUtc.Value.GetHashCode();
+ } else {
+ return 1;
+ }
+ }
+
+ /// <summary>
+ /// Serializes the applied policies for transmission from the Provider
+ /// to the Relying Party.
+ /// </summary>
+ /// <param name="policies">The applied policies.</param>
+ /// <returns>A space-delimited list of applied policies.</returns>
+ private static string SerializePolicies(IList<string> policies) {
+ if (policies.Count == 0) {
+ return AuthenticationPolicies.None;
+ } else {
+ return PapeUtilities.ConcatenateListOfElements(policies);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
new file mode 100644
index 0000000..18f63d4
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
@@ -0,0 +1,316 @@
+//-----------------------------------------------------------------------
+// <copyright file="ClaimsRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Carries the request/require/none demand state of the simple registration fields.
+ /// </summary>
+ [Serializable]
+ public sealed class ClaimsRequest : ExtensionBase {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == Constants.sreg_ns && isProviderRole) {
+ return new ClaimsRequest(typeUri);
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The type URI that this particular (deserialized) extension was read in using,
+ /// allowing a response to alter be crafted using the same type URI.
+ /// </summary>
+ private string typeUriDeserializedFrom;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClaimsRequest"/> class.
+ /// </summary>
+ public ClaimsRequest()
+ : base(new Version(1, 0), Constants.sreg_ns, Constants.AdditionalTypeUris) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClaimsRequest"/> class
+ /// by deserializing from a message.
+ /// </summary>
+ /// <param name="typeUri">The type URI this extension was recognized by in the OpenID message.</param>
+ internal ClaimsRequest(string typeUri)
+ : this() {
+ Requires.NotNullOrEmpty(typeUri, "typeUri");
+
+ this.typeUriDeserializedFrom = typeUri;
+ }
+
+ /// <summary>
+ /// Gets or sets the URL the consumer site provides for the authenticating user to review
+ /// for how his claims will be used by the consumer web site.
+ /// </summary>
+ [MessagePart(Constants.policy_url, IsRequired = false)]
+ public Uri PolicyUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the nickname of the user.
+ /// </summary>
+ public DemandLevel Nickname { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the email of the user.
+ /// </summary>
+ public DemandLevel Email { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the full name of the user.
+ /// </summary>
+ public DemandLevel FullName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the birthdate of the user.
+ /// </summary>
+ public DemandLevel BirthDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the gender of the user.
+ /// </summary>
+ public DemandLevel Gender { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the postal code of the user.
+ /// </summary>
+ public DemandLevel PostalCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the Country of the user.
+ /// </summary>
+ public DemandLevel Country { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the language of the user.
+ /// </summary>
+ public DemandLevel Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level of interest a relying party has in the time zone of the user.
+ /// </summary>
+ public DemandLevel TimeZone { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="ClaimsRequest"/> instance
+ /// is synthesized from an AX request at the Provider.
+ /// </summary>
+ internal bool Synthesized { get; set; }
+
+ /// <summary>
+ /// Gets or sets the value of the sreg.required parameter.
+ /// </summary>
+ /// <value>A comma-delimited list of sreg fields.</value>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")]
+ [MessagePart(Constants.required, AllowEmpty = true)]
+ private string RequiredList {
+ get { return string.Join(",", this.AssembleProfileFields(DemandLevel.Require)); }
+ set { this.SetProfileRequestFromList(value.Split(','), DemandLevel.Require); }
+ }
+
+ /// <summary>
+ /// Gets or sets the value of the sreg.optional parameter.
+ /// </summary>
+ /// <value>A comma-delimited list of sreg fields.</value>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")]
+ [MessagePart(Constants.optional, AllowEmpty = true)]
+ private string OptionalList {
+ get { return string.Join(",", this.AssembleProfileFields(DemandLevel.Request)); }
+ set { this.SetProfileRequestFromList(value.Split(','), DemandLevel.Request); }
+ }
+
+ /// <summary>
+ /// Tests equality between two <see cref="ClaimsRequest"/> structs.
+ /// </summary>
+ /// <param name="one">One instance to compare.</param>
+ /// <param name="other">Another instance to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator ==(ClaimsRequest one, ClaimsRequest other) {
+ return one.EqualsNullSafe(other);
+ }
+
+ /// <summary>
+ /// Tests inequality between two <see cref="ClaimsRequest"/> structs.
+ /// </summary>
+ /// <param name="one">One instance to compare.</param>
+ /// <param name="other">Another instance to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator !=(ClaimsRequest one, ClaimsRequest other) {
+ return !(one == other);
+ }
+
+ /// <summary>
+ /// Tests equality between two <see cref="ClaimsRequest"/> structs.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ ClaimsRequest other = obj as ClaimsRequest;
+ if (other == null) {
+ return false;
+ }
+
+ return
+ this.BirthDate.Equals(other.BirthDate) &&
+ this.Country.Equals(other.Country) &&
+ this.Language.Equals(other.Language) &&
+ this.Email.Equals(other.Email) &&
+ this.FullName.Equals(other.FullName) &&
+ this.Gender.Equals(other.Gender) &&
+ this.Nickname.Equals(other.Nickname) &&
+ this.PostalCode.Equals(other.PostalCode) &&
+ this.TimeZone.Equals(other.TimeZone) &&
+ this.PolicyUrl.EqualsNullSafe(other.PolicyUrl);
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ // It's important that if Equals returns true that the hash code also equals,
+ // so returning base.GetHashCode() is a BAD option.
+ // Return 1 is simple and poor for dictionary storage, but considering that every
+ // ClaimsRequest formulated at a single RP will likely have all the same fields,
+ // even a good hash code function will likely generate the same hash code. So
+ // we just cut to the chase and return a simple one.
+ return 1;
+ }
+
+ /// <summary>
+ /// Renders the requested information as a string.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override string ToString() {
+ string format = @"Nickname = '{0}'
+Email = '{1}'
+FullName = '{2}'
+Birthdate = '{3}'
+Gender = '{4}'
+PostalCode = '{5}'
+Country = '{6}'
+Language = '{7}'
+TimeZone = '{8}'";
+ return string.Format(CultureInfo.CurrentCulture, format, this.Nickname, this.Email, this.FullName, this.BirthDate, this.Gender, this.PostalCode, this.Country, this.Language, this.TimeZone);
+ }
+
+ /// <summary>
+ /// Prepares a Simple Registration response extension that is compatible with the
+ /// version of Simple Registration used in the request message.
+ /// </summary>
+ /// <returns>The newly created <see cref="ClaimsResponse"/> instance.</returns>
+ public ClaimsResponse CreateResponse() {
+ if (this.typeUriDeserializedFrom == null) {
+ throw new InvalidOperationException(OpenIdStrings.CallDeserializeBeforeCreateResponse);
+ }
+
+ return new ClaimsResponse(this.typeUriDeserializedFrom);
+ }
+
+ /// <summary>
+ /// Sets the profile request properties according to a list of
+ /// field names that might have been passed in the OpenId query dictionary.
+ /// </summary>
+ /// <param name="fieldNames">
+ /// The list of field names that should receive a given
+ /// <paramref name="requestLevel"/>. These field names should match
+ /// the OpenId specification for field names, omitting the 'openid.sreg' prefix.
+ /// </param>
+ /// <param name="requestLevel">The none/request/require state of the listed fields.</param>
+ internal void SetProfileRequestFromList(IEnumerable<string> fieldNames, DemandLevel requestLevel) {
+ foreach (string field in fieldNames) {
+ switch (field) {
+ case "": // this occurs for empty lists
+ break;
+ case Constants.nickname:
+ this.Nickname = requestLevel;
+ break;
+ case Constants.email:
+ this.Email = requestLevel;
+ break;
+ case Constants.fullname:
+ this.FullName = requestLevel;
+ break;
+ case Constants.dob:
+ this.BirthDate = requestLevel;
+ break;
+ case Constants.gender:
+ this.Gender = requestLevel;
+ break;
+ case Constants.postcode:
+ this.PostalCode = requestLevel;
+ break;
+ case Constants.country:
+ this.Country = requestLevel;
+ break;
+ case Constants.language:
+ this.Language = requestLevel;
+ break;
+ case Constants.timezone:
+ this.TimeZone = requestLevel;
+ break;
+ default:
+ Logger.OpenId.WarnFormat("ClaimsRequest.SetProfileRequestFromList: Unrecognized field name '{0}'.", field);
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Assembles the profile parameter names that have a given <see cref="DemandLevel"/>.
+ /// </summary>
+ /// <param name="level">The demand level (request, require, none).</param>
+ /// <returns>An array of the profile parameter names that meet the criteria.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")]
+ private string[] AssembleProfileFields(DemandLevel level) {
+ List<string> fields = new List<string>(10);
+ if (this.Nickname == level) {
+ fields.Add(Constants.nickname);
+ } if (this.Email == level) {
+ fields.Add(Constants.email);
+ } if (this.FullName == level) {
+ fields.Add(Constants.fullname);
+ } if (this.BirthDate == level) {
+ fields.Add(Constants.dob);
+ } if (this.Gender == level) {
+ fields.Add(Constants.gender);
+ } if (this.PostalCode == level) {
+ fields.Add(Constants.postcode);
+ } if (this.Country == level) {
+ fields.Add(Constants.country);
+ } if (this.Language == level) {
+ fields.Add(Constants.language);
+ } if (this.TimeZone == level) {
+ fields.Add(Constants.timezone);
+ }
+
+ return fields.ToArray();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
new file mode 100644
index 0000000..b50833b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
@@ -0,0 +1,357 @@
+//-----------------------------------------------------------------------
+// <copyright file="ClaimsResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Net.Mail;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Xml.Serialization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A struct storing Simple Registration field values describing an
+ /// authenticating user.
+ /// </summary>
+ [Serializable]
+ public sealed class ClaimsResponse : ExtensionBase, IClientScriptExtensionResponse, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if ((typeUri == Constants.sreg_ns || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) {
+ return new ClaimsResponse(typeUri);
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// The allowed format for birthdates.
+ /// </summary>
+ private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$");
+
+ /// <summary>
+ /// Storage for the raw string birthdate value.
+ /// </summary>
+ private string birthDateRaw;
+
+ /// <summary>
+ /// Backing field for the <see cref="BirthDate"/> property.
+ /// </summary>
+ private DateTime? birthDate;
+
+ /// <summary>
+ /// Backing field for the <see cref="Culture"/> property.
+ /// </summary>
+ private CultureInfo culture;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClaimsResponse"/> class.
+ /// </summary>
+ internal ClaimsResponse()
+ : this(Constants.sreg_ns) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClaimsResponse"/> class.
+ /// </summary>
+ /// <param name="typeUriToUse">
+ /// The type URI that must be used to identify this extension in the response message.
+ /// This value should be the same one the relying party used to send the extension request.
+ /// </param>
+ internal ClaimsResponse(string typeUriToUse)
+ : base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) {
+ Requires.NotNullOrEmpty(typeUriToUse, "typeUriToUse");
+ }
+
+ /// <summary>
+ /// Gets or sets the nickname the user goes by.
+ /// </summary>
+ [MessagePart(Constants.nickname)]
+ public string Nickname { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's email address.
+ /// </summary>
+ [MessagePart(Constants.email)]
+ public string Email { get; set; }
+
+ /// <summary>
+ /// Gets or sets the full name of a user as a single string.
+ /// </summary>
+ [MessagePart(Constants.fullname)]
+ public string FullName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's birthdate.
+ /// </summary>
+ public DateTime? BirthDate {
+ get {
+ return this.birthDate;
+ }
+
+ set {
+ this.birthDate = value;
+
+ // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
+ if (value.HasValue) {
+ this.birthDateRaw = value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ } else {
+ this.birthDateRaw = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the raw birth date string given by the extension.
+ /// </summary>
+ /// <value>A string in the format yyyy-MM-dd.</value>
+ [MessagePart(Constants.dob)]
+ public string BirthDateRaw {
+ get {
+ return this.birthDateRaw;
+ }
+
+ set {
+ ErrorUtilities.VerifyArgument(value == null || birthDateValidator.IsMatch(value), OpenIdStrings.SregInvalidBirthdate);
+ if (value != null) {
+ // Update the BirthDate property, if possible.
+ // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
+ // Some valid sreg dob values like "2000-00-00" will not work as a DateTime struct,
+ // in which case we null it out, but don't show any error.
+ DateTime newBirthDate;
+ if (DateTime.TryParse(value, out newBirthDate)) {
+ this.birthDate = newBirthDate;
+ } else {
+ Logger.OpenId.WarnFormat("Simple Registration birthdate '{0}' could not be parsed into a DateTime and may not include month and/or day information. Setting BirthDate property to null.", value);
+ this.birthDate = null;
+ }
+ } else {
+ this.birthDate = null;
+ }
+
+ this.birthDateRaw = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the gender of the user.
+ /// </summary>
+ [MessagePart(Constants.gender, Encoder = typeof(GenderEncoder))]
+ public Gender? Gender { get; set; }
+
+ /// <summary>
+ /// Gets or sets the zip code / postal code of the user.
+ /// </summary>
+ [MessagePart(Constants.postcode)]
+ public string PostalCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country of the user.
+ /// </summary>
+ [MessagePart(Constants.country)]
+ public string Country { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary/preferred language of the user.
+ /// </summary>
+ [MessagePart(Constants.language)]
+ public string Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's timezone.
+ /// </summary>
+ [MessagePart(Constants.timezone)]
+ public string TimeZone { get; set; }
+
+ /// <summary>
+ /// Gets a combination of the user's full name and email address.
+ /// </summary>
+ public MailAddress MailAddress {
+ get {
+ if (string.IsNullOrEmpty(this.Email)) {
+ return null;
+ } else if (string.IsNullOrEmpty(this.FullName)) {
+ return new MailAddress(this.Email);
+ } else {
+ return new MailAddress(this.Email, this.FullName);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a combination o the language and country of the user.
+ /// </summary>
+ [XmlIgnore]
+ public CultureInfo Culture {
+ get {
+ if (this.culture == null && !string.IsNullOrEmpty(this.Language)) {
+ string cultureString = string.Empty;
+ cultureString = this.Language;
+ if (!string.IsNullOrEmpty(this.Country)) {
+ cultureString += "-" + this.Country;
+ }
+ this.culture = CultureInfo.GetCultureInfo(cultureString);
+ }
+
+ return this.culture;
+ }
+
+ set {
+ this.culture = value;
+ this.Language = (value != null) ? value.TwoLetterISOLanguageName : null;
+ int indexOfHyphen = (value != null) ? value.Name.IndexOf('-') : -1;
+ this.Country = indexOfHyphen > 0 ? value.Name.Substring(indexOfHyphen + 1) : null;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this extension is signed by the Provider.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsSignedByProvider {
+ get { return this.IsSignedByRemoteParty; }
+ }
+
+ /// <summary>
+ /// Tests equality of two <see cref="ClaimsResponse"/> objects.
+ /// </summary>
+ /// <param name="one">One instance to compare.</param>
+ /// <param name="other">Another instance to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator ==(ClaimsResponse one, ClaimsResponse other) {
+ return one.EqualsNullSafe(other);
+ }
+
+ /// <summary>
+ /// Tests inequality of two <see cref="ClaimsResponse"/> objects.
+ /// </summary>
+ /// <param name="one">One instance to compare.</param>
+ /// <param name="other">Another instance to compare.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator !=(ClaimsResponse one, ClaimsResponse other) {
+ return !(one == other);
+ }
+
+ /// <summary>
+ /// Tests equality of two <see cref="ClaimsResponse"/> objects.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ ClaimsResponse other = obj as ClaimsResponse;
+ if (other == null) {
+ return false;
+ }
+
+ return
+ this.BirthDateRaw.EqualsNullSafe(other.BirthDateRaw) &&
+ this.Country.EqualsNullSafe(other.Country) &&
+ this.Language.EqualsNullSafe(other.Language) &&
+ this.Email.EqualsNullSafe(other.Email) &&
+ this.FullName.EqualsNullSafe(other.FullName) &&
+ this.Gender.Equals(other.Gender) &&
+ this.Nickname.EqualsNullSafe(other.Nickname) &&
+ this.PostalCode.EqualsNullSafe(other.PostalCode) &&
+ this.TimeZone.EqualsNullSafe(other.TimeZone);
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ return (this.Nickname != null) ? this.Nickname.GetHashCode() : base.GetHashCode();
+ }
+
+ #region IClientScriptExtension Members
+
+ /// <summary>
+ /// Reads the extension information on an authentication response from the provider.
+ /// </summary>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <returns>
+ /// A Javascript snippet that when executed on the user agent returns an object with
+ /// the information deserialized from the extension response.
+ /// </returns>
+ /// <remarks>
+ /// This method is called <b>before</b> the signature on the assertion response has been
+ /// verified. Therefore all information in these fields should be assumed unreliable
+ /// and potentially falsified.
+ /// </remarks>
+ string IClientScriptExtensionResponse.InitializeJavaScriptData(IProtocolMessageWithExtensions response) {
+ var sreg = new Dictionary<string, string>(15);
+
+ // Although we could probably whip up a trip with MessageDictionary
+ // to avoid explicitly setting each field, doing so would likely
+ // open ourselves up to security exploits from the OP as it would
+ // make possible sending arbitrary javascript in arbitrary field names.
+ sreg[Constants.nickname] = this.Nickname;
+ sreg[Constants.email] = this.Email;
+ sreg[Constants.fullname] = this.FullName;
+ sreg[Constants.dob] = this.BirthDateRaw;
+ sreg[Constants.gender] = this.Gender.HasValue ? this.Gender.Value.ToString() : null;
+ sreg[Constants.postcode] = this.PostalCode;
+ sreg[Constants.country] = this.Country;
+ sreg[Constants.language] = this.Language;
+ sreg[Constants.timezone] = this.TimeZone;
+
+ return MessagingUtilities.CreateJsonObject(sreg, false);
+ }
+
+ #endregion
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnSending() {
+ // Null out empty values so we don't send out a lot of empty parameters.
+ this.Country = EmptyToNull(this.Country);
+ this.Email = EmptyToNull(this.Email);
+ this.FullName = EmptyToNull(this.FullName);
+ this.Language = EmptyToNull(this.Language);
+ this.Nickname = EmptyToNull(this.Nickname);
+ this.PostalCode = EmptyToNull(this.PostalCode);
+ this.TimeZone = EmptyToNull(this.TimeZone);
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ void IMessageWithEvents.OnReceiving() {
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Translates an empty string value to null, or passes through non-empty values.
+ /// </summary>
+ /// <param name="value">The value to consider changing to null.</param>
+ /// <returns>Either null or a non-empty string.</returns>
+ private static string EmptyToNull(string value) {
+ return string.IsNullOrEmpty(value) ? null : value;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs
new file mode 100644
index 0000000..9e00137
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs
@@ -0,0 +1,46 @@
+// <auto-generated/> // disable StyleCop on this file
+//-----------------------------------------------------------------------
+// <copyright file="Constants.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ /// <summary>
+ /// Simple Registration constants
+ /// </summary>
+ internal static class Constants {
+ internal const string sreg_ns = "http://openid.net/extensions/sreg/1.1";
+ internal const string sreg_ns10 = "http://openid.net/sreg/1.0";
+ internal const string sreg_ns11other = "http://openid.net/sreg/1.1";
+ internal const string sreg_compatibility_alias = "sreg";
+ internal const string policy_url = "policy_url";
+ internal const string optional = "optional";
+ internal const string required = "required";
+ internal const string nickname = "nickname";
+ internal const string email = "email";
+ internal const string fullname = "fullname";
+ internal const string dob = "dob";
+ internal const string gender = "gender";
+ internal const string postcode = "postcode";
+ internal const string country = "country";
+ internal const string language = "language";
+ internal const string timezone = "timezone";
+ internal static class Genders {
+ internal const string Male = "M";
+ internal const string Female = "F";
+ }
+
+ /// <summary>
+ /// Additional type URIs that this extension is sometimes known by remote parties.
+ /// </summary>
+ internal static readonly string[] AdditionalTypeUris = new string[] {
+ Constants.sreg_ns10,
+ Constants.sreg_ns11other,
+ };
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs
new file mode 100644
index 0000000..7129270
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------
+// <copyright file="DemandLevel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
+ /// <summary>
+ /// Specifies what level of interest a relying party has in obtaining the value
+ /// of a given field offered by the Simple Registration extension.
+ /// </summary>
+ public enum DemandLevel {
+ /// <summary>
+ /// The relying party has no interest in obtaining this field.
+ /// </summary>
+ NoRequest,
+
+ /// <summary>
+ /// The relying party would like the value of this field, but wants
+ /// the Provider to display the field to the user as optionally provided.
+ /// </summary>
+ Request,
+
+ /// <summary>
+ /// The relying party considers this a required field as part of
+ /// authentication. The Provider and/or user agent MAY still choose to
+ /// not provide the value of the field however, according to the
+ /// Simple Registration extension specification.
+ /// </summary>
+ Require,
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs
new file mode 100644
index 0000000..979c481
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------
+// <copyright file="Gender.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
+ using System;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// Indicates the gender of a user.
+ /// </summary>
+ public enum Gender {
+ /// <summary>
+ /// The user is male.
+ /// </summary>
+ Male,
+
+ /// <summary>
+ /// The user is female.
+ /// </summary>
+ Female,
+ }
+
+ /// <summary>
+ /// Encodes/decodes the Simple Registration Gender type to its string representation.
+ /// </summary>
+ internal class GenderEncoder : IMessagePartEncoder {
+ #region IMessagePartEncoder Members
+
+ /// <summary>
+ /// Encodes the specified value.
+ /// </summary>
+ /// <param name="value">The value. Guaranteed to never be null.</param>
+ /// <returns>
+ /// The <paramref name="value"/> in string form, ready for message transport.
+ /// </returns>
+ public string Encode(object value) {
+ var gender = (Gender?)value;
+ if (gender.HasValue) {
+ switch (gender.Value) {
+ case Gender.Male: return Constants.Genders.Male;
+ case Gender.Female: return Constants.Genders.Female;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Decodes the specified value.
+ /// </summary>
+ /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param>
+ /// <returns>
+ /// The deserialized form of the given string.
+ /// </returns>
+ /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception>
+ public object Decode(string value) {
+ switch (value) {
+ case Constants.Genders.Male: return SimpleRegistration.Gender.Male;
+ case Constants.Genders.Female: return SimpleRegistration.Gender.Female;
+ default: throw new FormatException();
+ }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs
new file mode 100644
index 0000000..1dcda27
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs
@@ -0,0 +1,97 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardOpenIdExtensionFactory.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.OAuth;
+ using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// An OpenID extension factory that supports registration so that third-party
+ /// extensions can add themselves to this library's supported extension list.
+ /// </summary>
+ internal class StandardOpenIdExtensionFactory : IOpenIdExtensionFactory {
+ /// <summary>
+ /// A collection of the registered OpenID extensions.
+ /// </summary>
+ private List<CreateDelegate> registeredExtensions = new List<CreateDelegate>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardOpenIdExtensionFactory"/> class.
+ /// </summary>
+ internal StandardOpenIdExtensionFactory() {
+ this.RegisterExtension(ClaimsRequest.Factory);
+ this.RegisterExtension(ClaimsResponse.Factory);
+ this.RegisterExtension(FetchRequest.Factory);
+ this.RegisterExtension(FetchResponse.Factory);
+ this.RegisterExtension(StoreRequest.Factory);
+ this.RegisterExtension(StoreResponse.Factory);
+ this.RegisterExtension(PolicyRequest.Factory);
+ this.RegisterExtension(PolicyResponse.Factory);
+ this.RegisterExtension(AuthorizationRequest.Factory);
+ this.RegisterExtension(AuthorizationApprovedResponse.Factory);
+ this.RegisterExtension(AuthorizationDeclinedResponse.Factory);
+ this.RegisterExtension(UIRequest.Factory);
+ }
+
+ /// <summary>
+ /// A delegate that individual extensions may register with this factory.
+ /// </summary>
+ /// <param name="typeUri">The type URI of the extension.</param>
+ /// <param name="data">The parameters associated specifically with this extension.</param>
+ /// <param name="baseMessage">The OpenID message carrying this extension.</param>
+ /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param>
+ /// <returns>
+ /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes
+ /// the extension described in the input parameters; <c>null</c> otherwise.
+ /// </returns>
+ internal delegate IOpenIdMessageExtension CreateDelegate(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole);
+
+ #region IOpenIdExtensionFactory Members
+
+ /// <summary>
+ /// Creates a new instance of some extension based on the received extension parameters.
+ /// </summary>
+ /// <param name="typeUri">The type URI of the extension.</param>
+ /// <param name="data">The parameters associated specifically with this extension.</param>
+ /// <param name="baseMessage">The OpenID message carrying this extension.</param>
+ /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param>
+ /// <returns>
+ /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes
+ /// the extension described in the input parameters; <c>null</c> otherwise.
+ /// </returns>
+ /// <remarks>
+ /// This factory method need only initialize properties in the instantiated extension object
+ /// that are not bound using <see cref="MessagePartAttribute"/>.
+ /// </remarks>
+ public IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) {
+ foreach (var factoryMethod in this.registeredExtensions) {
+ IOpenIdMessageExtension result = factoryMethod(typeUri, data, baseMessage, isProviderRole);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Registers a new extension delegate.
+ /// </summary>
+ /// <param name="creator">The factory method that can create the extension.</param>
+ internal void RegisterExtension(CreateDelegate creator) {
+ this.registeredExtensions.Add(creator);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs
new file mode 100644
index 0000000..1cc920a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs
@@ -0,0 +1,34 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIConstants.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.UI {
+ /// <summary>
+ /// Constants used to support the UI extension.
+ /// </summary>
+ internal static class UIConstants {
+ /// <summary>
+ /// The type URI associated with this extension.
+ /// </summary>
+ internal const string UITypeUri = "http://specs.openid.net/extensions/ui/1.0";
+
+ /// <summary>
+ /// The Type URI that appears in an XRDS document when the OP supports popups through the UI extension.
+ /// </summary>
+ internal const string PopupSupported = "http://specs.openid.net/extensions/ui/1.0/mode/popup";
+
+ /// <summary>
+ /// The Type URI that appears in an XRDS document when the OP supports the RP
+ /// specifying the user's preferred language through the UI extension.
+ /// </summary>
+ internal const string LangPrefSupported = "http://specs.openid.net/extensions/ui/1.0/lang-pref";
+
+ /// <summary>
+ /// The Type URI that appears in the XRDS document when the OP supports the RP
+ /// specifying the icon for the OP to display during authentication through the UI extension.
+ /// </summary>
+ internal const string IconSupported = "http://specs.openid.net/extensions/ui/1.0/icon";
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs
new file mode 100644
index 0000000..8e3e20f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIModes.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.UI {
+ /// <summary>
+ /// Valid values for the <c>mode</c> parameter of the OpenID User Interface extension.
+ /// </summary>
+ public static class UIModes {
+ /// <summary>
+ /// Indicates that the Provider's authentication page appears in a popup window.
+ /// </summary>
+ /// <value>The constant <c>"popup"</c>.</value>
+ /// <remarks>
+ /// <para>The RP SHOULD create the popup to be 450 pixels wide and 500 pixels tall. The popup MUST have the address bar displayed, and MUST be in a standalone browser window. The contents of the popup MUST NOT be framed by the RP. </para>
+ /// <para>The RP SHOULD open the popup centered above the main browser window, and SHOULD dim the contents of the parent window while the popup is active. The RP SHOULD ensure that the user is not surprised by the appearance of the popup, and understands how to interact with it. </para>
+ /// <para>To keep the user popup user experience consistent, it is RECOMMENDED that the OP does not resize the popup window unless the OP requires additional space to show special features that are not usually displayed as part of the default popup user experience. </para>
+ /// <para>The OP MAY close the popup without returning a response to the RP. Closing the popup without sending a response should be interpreted as a negative assertion. </para>
+ /// <para>The response to an authentication request in a popup is unchanged from [OpenID 2.0] (OpenID 2.0 Workgroup, “OpenID 2.0,” .). Relying Parties detecting that the popup was closed without receiving an authentication response SHOULD interpret the close event to be a negative assertion. </para>
+ /// </remarks>
+ public const string Popup = "popup";
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs
new file mode 100644
index 0000000..9d506ca
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs
@@ -0,0 +1,197 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.UI {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.Xrds;
+
+ /// <summary>
+ /// OpenID User Interface extension 1.0 request message.
+ /// </summary>
+ /// <remarks>
+ /// <para>Implements the extension described by: http://wiki.openid.net/f/openid_ui_extension_draft01.html </para>
+ /// <para>This extension only applies to checkid_setup requests, since checkid_immediate requests display
+ /// no UI to the user. </para>
+ /// <para>For rules about how the popup window should be displayed, please see the documentation of
+ /// <see cref="UIModes.Popup"/>. </para>
+ /// <para>An RP may determine whether an arbitrary OP supports this extension (and thereby determine
+ /// whether to use a standard full window redirect or a popup) via the
+ /// <see cref="IdentifierDiscoveryResult.IsExtensionSupported&lt;T&gt;()"/> method.</para>
+ /// </remarks>
+ [Serializable]
+ public class UIRequest : IOpenIdMessageExtension, IMessageWithEvents {
+ /// <summary>
+ /// The factory method that may be used in deserialization of this message.
+ /// </summary>
+ internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
+ if (typeUri == UIConstants.UITypeUri && isProviderRole) {
+ return new UIRequest();
+ }
+
+ return null;
+ };
+
+ /// <summary>
+ /// Additional type URIs that this extension is sometimes known by remote parties.
+ /// </summary>
+ private static readonly string[] additionalTypeUris = new string[] {
+ UIConstants.LangPrefSupported,
+ UIConstants.PopupSupported,
+ UIConstants.IconSupported,
+ };
+
+ /// <summary>
+ /// Backing store for <see cref="ExtraData"/>.
+ /// </summary>
+ private Dictionary<string, string> extraData = new Dictionary<string, string>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UIRequest"/> class.
+ /// </summary>
+ public UIRequest() {
+ this.LanguagePreference = new[] { CultureInfo.CurrentUICulture };
+ this.Mode = UIModes.Popup;
+ }
+
+ /// <summary>
+ /// Gets or sets the list of user's preferred languages, sorted in decreasing preferred order.
+ /// </summary>
+ /// <value>The default is the <see cref="CultureInfo.CurrentUICulture"/> of the thread that created this instance.</value>
+ /// <remarks>
+ /// The user's preferred languages as a [BCP 47] language priority list, represented as a comma-separated list of BCP 47 basic language ranges in descending priority order. For instance, the value "fr-CA,fr-FR,en-CA" represents the preference for French spoken in Canada, French spoken in France, followed by English spoken in Canada.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "By design.")]
+ [MessagePart("lang", AllowEmpty = false)]
+ public CultureInfo[] LanguagePreference { get; set; }
+
+ /// <summary>
+ /// Gets or sets the style of UI that the RP is hosting the OP's authentication page in.
+ /// </summary>
+ /// <value>Some value from the <see cref="UIModes"/> class. Defaults to <see cref="UIModes.Popup"/>.</value>
+ [MessagePart("mode", AllowEmpty = false, IsRequired = true)]
+ public string Mode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the Relying Party has an icon
+ /// it would like the Provider to display to the user while asking them
+ /// whether they would like to log in.
+ /// </summary>
+ /// <value><c>true</c> if the Provider should display an icon; otherwise, <c>false</c>.</value>
+ /// <remarks>
+ /// By default, the Provider displays the relying party's favicon.ico.
+ /// </remarks>
+ [MessagePart("icon", AllowEmpty = false, IsRequired = false)]
+ public bool? Icon { get; set; }
+
+ #region IOpenIdMessageExtension Members
+
+ /// <summary>
+ /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements.
+ /// </summary>
+ /// <value></value>
+ public string TypeUri { get { return UIConstants.UITypeUri; } }
+
+ /// <summary>
+ /// Gets the additional TypeURIs that are supported by this extension, in preferred order.
+ /// May be empty if none other than <see cref="TypeUri"/> is supported, but
+ /// should not be null.
+ /// </summary>
+ /// <remarks>
+ /// Useful for reading in messages with an older version of an extension.
+ /// The value in the <see cref="TypeUri"/> property is always checked before
+ /// trying this list.
+ /// If you do support multiple versions of an extension using this method,
+ /// consider adding a CreateResponse method to your request extension class
+ /// so that the response can have the context it needs to remain compatible
+ /// given the version of the extension in the request message.
+ /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example.
+ /// </remarks>
+ public IEnumerable<string> AdditionalSupportedTypeUris { get { return additionalTypeUris; } }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this extension was
+ /// signed by the sender.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is signed by the sender; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsSignedByRemoteParty { get; set; }
+
+ #endregion
+
+ #region IMessage Properties
+
+ /// <summary>
+ /// Gets the version of the protocol or extension this message is prepared to implement.
+ /// </summary>
+ /// <value>The value 1.0.</value>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ public Version Version {
+ get { return new Version(1, 0); }
+ }
+
+ /// <summary>
+ /// Gets the extra, non-standard Protocol parameters included in the message.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this interface should ensure that this property never returns null.
+ /// </remarks>
+ public IDictionary<string, string> ExtraData {
+ get { return this.extraData; }
+ }
+
+ #endregion
+
+ #region IMessage methods
+
+ /// <summary>
+ /// Checks the message state for conformity to the protocol specification
+ /// and throws an exception if the message is invalid.
+ /// </summary>
+ /// <remarks>
+ /// <para>Some messages have required fields, or combinations of fields that must relate to each other
+ /// in specialized ways. After deserializing a message, this method checks the state of the
+ /// message to see if it conforms to the protocol.</para>
+ /// <para>Note that this property should <i>not</i> check signatures or perform any state checks
+ /// outside this scope of this particular message.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
+ public void EnsureValidMessage() {
+ }
+
+ #endregion
+
+ #region IMessageWithEvents Members
+
+ /// <summary>
+ /// Called when the message is about to be transmitted,
+ /// before it passes through the channel binding elements.
+ /// </summary>
+ public void OnSending() {
+ }
+
+ /// <summary>
+ /// Called when the message has been received,
+ /// after it passes through the channel binding elements.
+ /// </summary>
+ public void OnReceiving() {
+ if (this.LanguagePreference != null) {
+ // TODO: see if we can change the CultureInfo.CurrentUICulture somehow
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs
new file mode 100644
index 0000000..478666b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs
@@ -0,0 +1,28 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.UI {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Constants used in implementing support for the UI extension.
+ /// </summary>
+ public static class UIUtilities {
+ /// <summary>
+ /// The required width of the popup window the relying party creates for the provider.
+ /// </summary>
+ public const int PopupWidth = 500; // UI extension calls for 450px, but Yahoo needs 500px
+
+ /// <summary>
+ /// The required height of the popup window the relying party creates for the provider.
+ /// </summary>
+ public const int PopupHeight = 500;
+ }
+}