summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/ChannelElements')
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs129
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs251
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs48
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs44
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs169
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs228
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs47
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs210
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs199
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs64
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs87
11 files changed, 1476 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs
new file mode 100644
index 0000000..b730b1f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs
@@ -0,0 +1,129 @@
+//-----------------------------------------------------------------------
+// <copyright file="BackwardCompatibilityBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Provides a mechanism for Relying Parties to work with OpenID 1.0 Providers
+ /// without losing claimed_id and op_endpoint data, which OpenID 2.0 Providers
+ /// are required to send back with positive assertions.
+ /// </summary>
+ internal class BackwardCompatibilityBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// The "dnoa.op_endpoint" callback parameter that stores the Provider Endpoint URL
+ /// to tack onto the return_to URI.
+ /// </summary>
+ private const string ProviderEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint";
+
+ /// <summary>
+ /// The "dnoa.claimed_id" callback parameter that stores the Claimed Identifier
+ /// to tack onto the return_to URI.
+ /// </summary>
+ private const string ClaimedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id";
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.None"/></value>
+ public MessageProtections Protection {
+ get { return MessageProtections.None; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ SignedResponseRequest request = message as SignedResponseRequest;
+ if (request != null && request.Version.Major < 2) {
+ request.AddReturnToArguments(ProviderEndpointParameterName, request.Recipient.AbsoluteUri);
+
+ CheckIdRequest authRequest = request as CheckIdRequest;
+ if (authRequest != null) {
+ request.AddReturnToArguments(ClaimedIdentifierParameterName, authRequest.ClaimedIdentifier);
+ }
+
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ IndirectSignedResponse response = message as IndirectSignedResponse;
+ if (response != null && response.Version.Major < 2) {
+ // GetReturnToArgument may return parameters that are not signed,
+ // but we must allow for that since in OpenID 1.x, a stateless RP has
+ // no way to preserve the provider endpoint and claimed identifier otherwise.
+ // We'll verify the positive assertion later in the
+ // RelyingParty.PositiveAuthenticationResponse constructor anyway.
+ // If this is a 1.0 OP signed response without these parameters then we didn't initiate
+ // the request ,and since 1.0 OPs are not supposed to be able to send unsolicited
+ // assertions it's an invalid case that we throw an exception for.
+ if (response.ProviderEndpoint == null) {
+ string op_endpoint = response.GetReturnToArgument(ProviderEndpointParameterName);
+ ErrorUtilities.VerifyProtocol(op_endpoint != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ProviderEndpointParameterName);
+ response.ProviderEndpoint = new Uri(op_endpoint);
+ }
+
+ PositiveAssertionResponse authResponse = response as PositiveAssertionResponse;
+ if (authResponse != null) {
+ if (authResponse.ClaimedIdentifier == null) {
+ string claimedId = response.GetReturnToArgument(ClaimedIdentifierParameterName);
+ ErrorUtilities.VerifyProtocol(claimedId != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ClaimedIdentifierParameterName);
+ authResponse.ClaimedIdentifier = claimedId;
+ }
+ }
+
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs
new file mode 100644
index 0000000..705f737
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs
@@ -0,0 +1,251 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExtensionsBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// The binding element that serializes/deserializes OpenID extensions to/from
+ /// their carrying OpenID messages.
+ /// </summary>
+ internal class ExtensionsBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// False if unsigned extensions should be dropped. Must always be true on Providers, since RPs never sign extensions.
+ /// </summary>
+ private readonly bool receiveUnsignedExtensions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExtensionsBindingElement"/> class.
+ /// </summary>
+ /// <param name="extensionFactory">The extension factory.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ /// <param name="receiveUnsignedExtensions">Security setting for relying parties. Should be true for Providers.</param>
+ internal ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory, SecuritySettings securitySettings, bool receiveUnsignedExtensions) {
+ Requires.NotNull(extensionFactory, "extensionFactory");
+ Requires.NotNull(securitySettings, "securitySettings");
+
+ this.ExtensionFactory = extensionFactory;
+ this.receiveUnsignedExtensions = receiveUnsignedExtensions;
+ }
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the extension factory.
+ /// </summary>
+ public IOpenIdExtensionFactory ExtensionFactory { get; private set; }
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.None"/></value>
+ public MessageProtections Protection {
+ get { return MessageProtections.None; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "It doesn't look too bad to me. :)")]
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ var extendableMessage = message as IProtocolMessageWithExtensions;
+ if (extendableMessage != null) {
+ Protocol protocol = Protocol.Lookup(message.Version);
+ MessageDictionary baseMessageDictionary = this.Channel.MessageDescriptions.GetAccessor(message);
+
+ // We have a helper class that will do all the heavy-lifting of organizing
+ // all the extensions, their aliases, and their parameters.
+ var extensionManager = ExtensionArgumentsManager.CreateOutgoingExtensions(protocol);
+ foreach (IExtensionMessage protocolExtension in extendableMessage.Extensions) {
+ var extension = protocolExtension as IOpenIdMessageExtension;
+ if (extension != null) {
+ Reporting.RecordFeatureUse(protocolExtension);
+
+ // Give extensions that require custom serialization a chance to do their work.
+ var customSerializingExtension = extension as IMessageWithEvents;
+ if (customSerializingExtension != null) {
+ customSerializingExtension.OnSending();
+ }
+
+ // OpenID 2.0 Section 12 forbids two extensions with the same TypeURI in the same message.
+ ErrorUtilities.VerifyProtocol(!extensionManager.ContainsExtension(extension.TypeUri), OpenIdStrings.ExtensionAlreadyAddedWithSameTypeURI, extension.TypeUri);
+
+ // Ensure that we're sending out a valid extension.
+ var extensionDescription = this.Channel.MessageDescriptions.Get(extension);
+ var extensionDictionary = extensionDescription.GetDictionary(extension).Serialize();
+ extensionDescription.EnsureMessagePartsPassBasicValidation(extensionDictionary);
+
+ // Add the extension to the outgoing message payload.
+ extensionManager.AddExtensionArguments(extension.TypeUri, extensionDictionary);
+ } else {
+ Logger.OpenId.WarnFormat("Unexpected extension type {0} did not implement {1}.", protocolExtension.GetType(), typeof(IOpenIdMessageExtension).Name);
+ }
+ }
+
+ // We use a cheap trick (for now at least) to determine whether the 'openid.' prefix
+ // belongs on the parameters by just looking at what other parameters do.
+ // Technically, direct message responses from Provider to Relying Party are the only
+ // messages that leave off the 'openid.' prefix.
+ bool includeOpenIdPrefix = baseMessageDictionary.Keys.Any(key => key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal));
+
+ // Add the extension parameters to the base message for transmission.
+ baseMessageDictionary.AddExtraParameters(extensionManager.GetArgumentsToSend(includeOpenIdPrefix));
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ var extendableMessage = message as IProtocolMessageWithExtensions;
+ if (extendableMessage != null) {
+ // First add the extensions that are signed by the Provider.
+ foreach (IOpenIdMessageExtension signedExtension in this.GetExtensions(extendableMessage, true, null)) {
+ Reporting.RecordFeatureUse(signedExtension);
+ signedExtension.IsSignedByRemoteParty = true;
+ extendableMessage.Extensions.Add(signedExtension);
+ }
+
+ // Now search again, considering ALL extensions whether they are signed or not,
+ // skipping the signed ones and adding the new ones as unsigned extensions.
+ if (this.receiveUnsignedExtensions) {
+ Func<string, bool> isNotSigned = typeUri => !extendableMessage.Extensions.Cast<IOpenIdMessageExtension>().Any(ext => ext.TypeUri == typeUri);
+ foreach (IOpenIdMessageExtension unsignedExtension in this.GetExtensions(extendableMessage, false, isNotSigned)) {
+ Reporting.RecordFeatureUse(unsignedExtension);
+ unsignedExtension.IsSignedByRemoteParty = false;
+ extendableMessage.Extensions.Add(unsignedExtension);
+ }
+ }
+
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the extensions on a message.
+ /// </summary>
+ /// <param name="message">The carrier of the extensions.</param>
+ /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param>
+ /// <param name="extensionFilter">A optional filter that takes an extension type URI and
+ /// returns a value indicating whether that extension should be deserialized and
+ /// returned in the sequence. May be null.</param>
+ /// <returns>A sequence of extensions in the message.</returns>
+ private IEnumerable<IOpenIdMessageExtension> GetExtensions(IProtocolMessageWithExtensions message, bool ignoreUnsigned, Func<string, bool> extensionFilter) {
+ bool isAtProvider = message is SignedResponseRequest;
+
+ // We have a helper class that will do all the heavy-lifting of organizing
+ // all the extensions, their aliases, and their parameters.
+ var extensionManager = ExtensionArgumentsManager.CreateIncomingExtensions(this.GetExtensionsDictionary(message, ignoreUnsigned));
+ foreach (string typeUri in extensionManager.GetExtensionTypeUris()) {
+ // Our caller may have already obtained a signed version of this extension,
+ // so skip it if they don't want this one.
+ if (extensionFilter != null && !extensionFilter(typeUri)) {
+ continue;
+ }
+
+ var extensionData = extensionManager.GetExtensionArguments(typeUri);
+
+ // Initialize this particular extension.
+ IOpenIdMessageExtension extension = this.ExtensionFactory.Create(typeUri, extensionData, message, isAtProvider);
+ if (extension != null) {
+ try {
+ // Make sure the extension fulfills spec requirements before deserializing it.
+ MessageDescription messageDescription = this.Channel.MessageDescriptions.Get(extension);
+ messageDescription.EnsureMessagePartsPassBasicValidation(extensionData);
+
+ // Deserialize the extension.
+ MessageDictionary extensionDictionary = messageDescription.GetDictionary(extension);
+ foreach (var pair in extensionData) {
+ extensionDictionary[pair.Key] = pair.Value;
+ }
+
+ // Give extensions that require custom serialization a chance to do their work.
+ var customSerializingExtension = extension as IMessageWithEvents;
+ if (customSerializingExtension != null) {
+ customSerializingExtension.OnReceiving();
+ }
+ } catch (ProtocolException ex) {
+ Logger.OpenId.ErrorFormat(OpenIdStrings.BadExtension, extension.GetType(), ex);
+ extension = null;
+ }
+
+ if (extension != null) {
+ yield return extension;
+ }
+ } else {
+ Logger.OpenId.DebugFormat("Extension with type URI '{0}' ignored because it is not a recognized extension.", typeUri);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the dictionary of message parts that should be deserialized into extensions.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param>
+ /// <returns>
+ /// A dictionary of message parts, including only signed parts when appropriate.
+ /// </returns>
+ private IDictionary<string, string> GetExtensionsDictionary(IProtocolMessage message, bool ignoreUnsigned) {
+ Requires.ValidState(this.Channel != null);
+
+ IndirectSignedResponse signedResponse = message as IndirectSignedResponse;
+ if (signedResponse != null && ignoreUnsigned) {
+ return signedResponse.GetSignedMessageParts(this.Channel);
+ } else {
+ return this.Channel.MessageDescriptions.GetAccessor(message);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs
new file mode 100644
index 0000000..0c8d95e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------
+// <copyright file="IOpenIdExtensionFactory.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// OpenID extension factory class for creating extensions based on received Type URIs.
+ /// </summary>
+ /// <remarks>
+ /// OpenID extension factories must be registered with the library. This can be
+ /// done by adding a factory to OpenIdRelyingParty.ExtensionFactories
+ /// or OpenIdProvider.ExtensionFactories, or by adding a snippet
+ /// such as the following to your web.config file:
+ /// <example>
+ /// &lt;dotNetOpenAuth&gt;
+ /// &lt;openid&gt;
+ /// &lt;extensionFactories&gt;
+ /// &lt;add type="DotNetOpenAuth.ApplicationBlock.CustomExtensions.Acme, DotNetOpenAuth.ApplicationBlock" /&gt;
+ /// &lt;/extensionFactories&gt;
+ /// &lt;/openid&gt;
+ /// &lt;/dotNetOpenAuth&gt;
+ /// </example>
+ /// </remarks>
+ public interface IOpenIdExtensionFactory {
+ /// <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>
+ IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole);
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs
new file mode 100644
index 0000000..533e818
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------
+// <copyright file="ITamperResistantOpenIdMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An interface that OAuth messages implement to support signing.
+ /// </summary>
+ internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage, IReplayProtectedProtocolMessage {
+ /// <summary>
+ /// Gets or sets the association handle used to sign the message.
+ /// </summary>
+ /// <value>The handle for the association that was used to sign this assertion. </value>
+ string AssociationHandle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more.
+ /// </summary>
+ /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value>
+ string InvalidateHandle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the signed parameter order.
+ /// </summary>
+ /// <value>Comma-separated list of signed fields.</value>
+ /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example>
+ /// <remarks>
+ /// This entry consists of the fields without the "openid." prefix that the signature covers.
+ /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle",
+ /// and if present in the response, "claimed_id" and "identity".
+ /// Additional keys MAY be signed as part of the message. See Generating Signatures.
+ /// </remarks>
+ string SignedParameterOrder { get; set; } // TODO: make sure we have a unit test to verify that an incoming message with fewer signed fields than required will be rejected.
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs
new file mode 100644
index 0000000..1993cb4
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs
@@ -0,0 +1,169 @@
+//-----------------------------------------------------------------------
+// <copyright file="KeyValueFormEncoding.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Indicates the level of strictness to require when decoding a
+ /// Key-Value Form encoded dictionary.
+ /// </summary>
+ public enum KeyValueFormConformanceLevel {
+ /// <summary>
+ /// Be as forgiving as possible to errors made while encoding.
+ /// </summary>
+ Loose,
+
+ /// <summary>
+ /// Allow for certain errors in encoding attributable to ambiguities
+ /// in the OpenID 1.1 spec's description of the encoding.
+ /// </summary>
+ OpenId11,
+
+ /// <summary>
+ /// The strictest mode. The decoder requires the encoded dictionary
+ /// to be in strict compliance with OpenID 2.0's description of
+ /// the encoding.
+ /// </summary>
+ OpenId20,
+ }
+
+ /// <summary>
+ /// Performs conversion to and from the Key-Value Form Encoding defined by
+ /// OpenID Authentication 2.0 section 4.1.1.
+ /// http://openid.net/specs/openid-authentication-2_0.html#anchor4
+ /// </summary>
+ /// <remarks>
+ /// This class is thread safe and immutable.
+ /// </remarks>
+ internal class KeyValueFormEncoding {
+ /// <summary>
+ /// Characters that must not appear in parameter names.
+ /// </summary>
+ private static readonly char[] IllegalKeyCharacters = { '\n', ':' };
+
+ /// <summary>
+ /// Characters that must not appaer in parameter values.
+ /// </summary>
+ private static readonly char[] IllegalValueCharacters = { '\n' };
+
+ /// <summary>
+ /// The newline character sequence to use.
+ /// </summary>
+ private const string NewLineCharacters = "\n";
+
+ /// <summary>
+ /// The character encoding to use.
+ /// </summary>
+ private static readonly Encoding textEncoding = new UTF8Encoding(false);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class.
+ /// </summary>
+ public KeyValueFormEncoding() {
+ this.ConformanceLevel = KeyValueFormConformanceLevel.Loose;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class.
+ /// </summary>
+ /// <param name="conformanceLevel">How strictly an incoming Key-Value Form message will be held to the spec.</param>
+ public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) {
+ this.ConformanceLevel = conformanceLevel;
+ }
+
+ /// <summary>
+ /// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec.
+ /// </summary>
+ public KeyValueFormConformanceLevel ConformanceLevel { get; private set; }
+
+ /// <summary>
+ /// Encodes key/value pairs to Key-Value Form.
+ /// </summary>
+ /// <param name="keysAndValues">
+ /// The dictionary of key/value pairs to convert to a byte stream.
+ /// </param>
+ /// <returns>The UTF8 byte array.</returns>
+ /// <remarks>
+ /// Enumerating a Dictionary&lt;TKey, TValue&gt; has undeterministic ordering.
+ /// If ordering of the key=value pairs is important, a deterministic enumerator must
+ /// be used.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")]
+ public static byte[] GetBytes(IEnumerable<KeyValuePair<string, string>> keysAndValues) {
+ Requires.NotNull(keysAndValues, "keysAndValues");
+
+ using (MemoryStream ms = new MemoryStream()) {
+ using (StreamWriter sw = new StreamWriter(ms, textEncoding)) {
+ sw.NewLine = NewLineCharacters;
+ foreach (var pair in keysAndValues) {
+ if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key));
+ }
+ if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value));
+ }
+
+ sw.Write(pair.Key);
+ sw.Write(':');
+ sw.Write(pair.Value);
+ sw.WriteLine();
+ }
+ }
+
+ return ms.ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Decodes bytes in Key-Value Form to key/value pairs.
+ /// </summary>
+ /// <param name="data">The stream of Key-Value Form encoded bytes.</param>
+ /// <returns>The deserialized dictionary.</returns>
+ /// <exception cref="FormatException">Thrown when the data is not in the expected format.</exception>
+ public IDictionary<string, string> GetDictionary(Stream data) {
+ using (StreamReader reader = new StreamReader(data, textEncoding)) {
+ var dict = new Dictionary<string, string>();
+ int line_num = 0;
+ string line;
+ while ((line = reader.ReadLine()) != null) {
+ line_num++;
+ if (this.ConformanceLevel == KeyValueFormConformanceLevel.Loose) {
+ line = line.Trim();
+ if (line.Length == 0) {
+ continue;
+ }
+ }
+ string[] parts = line.Split(new[] { ':' }, 2);
+ ErrorUtilities.VerifyFormat(parts.Length == 2, OpenIdStrings.InvalidKeyValueFormCharacterMissing, ':', line_num, line);
+ if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
+ ErrorUtilities.VerifyFormat(!(char.IsWhiteSpace(parts[0], parts[0].Length - 1) || char.IsWhiteSpace(parts[1], 0)), OpenIdStrings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line);
+ }
+ if (this.ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) {
+ parts[0] = parts[0].Trim();
+ parts[1] = parts[1].Trim();
+ }
+
+ // calling Add method will throw if a key is encountered twice,
+ // which we should do.
+ dict.Add(parts[0], parts[1]);
+ }
+ if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
+ reader.BaseStream.Seek(-1, SeekOrigin.End);
+ ErrorUtilities.VerifyFormat(reader.BaseStream.ReadByte() == '\n', OpenIdStrings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line);
+ }
+ return dict;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs
new file mode 100644
index 0000000..a2a5c88
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs
@@ -0,0 +1,228 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdChannel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A channel that knows how to send and receive OpenID messages.
+ /// </summary>
+ [ContractVerification(true)]
+ internal class OpenIdChannel : Channel {
+ /// <summary>
+ /// The HTTP Content-Type to use in Key-Value Form responses.
+ /// </summary>
+ /// <remarks>
+ /// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain. But this value
+ /// does not prevent free hosters like GoDaddy from tacking on their ads
+ /// to the end of the direct response, corrupting the data. So we deviate
+ /// from the spec a bit here to improve the story for free Providers.
+ /// </remarks>
+ internal const string KeyValueFormContentType = "application/x-openid-kvf";
+
+ /// <summary>
+ /// The encoder that understands how to read and write Key-Value Form.
+ /// </summary>
+ private KeyValueFormEncoding keyValueForm = new KeyValueFormEncoding();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdChannel"/> class.
+ /// </summary>
+ /// <param name="messageTypeProvider">A class prepared to analyze incoming messages and indicate what concrete
+ /// message types can deserialize from it.</param>
+ /// <param name="bindingElements">The binding elements to use in sending and receiving messages.</param>
+ protected OpenIdChannel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements)
+ : base(messageTypeProvider, bindingElements) {
+ Requires.NotNull(messageTypeProvider, "messageTypeProvider");
+
+ // Customize the binding element order, since we play some tricks for higher
+ // security and backward compatibility with older OpenID versions.
+ var outgoingBindingElements = new List<IChannelBindingElement>(bindingElements);
+ var incomingBindingElements = new List<IChannelBindingElement>(bindingElements);
+ incomingBindingElements.Reverse();
+
+ // Customize the order of the incoming elements by moving the return_to elements in front.
+ var backwardCompatibility = incomingBindingElements.OfType<BackwardCompatibilityBindingElement>().SingleOrDefault();
+ var returnToSign = incomingBindingElements.OfType<ReturnToSignatureBindingElement>().SingleOrDefault();
+ if (backwardCompatibility != null) {
+ incomingBindingElements.MoveTo(0, backwardCompatibility);
+ }
+ if (returnToSign != null) {
+ // Yes, this is intentionally, shifting the backward compatibility
+ // binding element to second position.
+ incomingBindingElements.MoveTo(0, returnToSign);
+ }
+
+ this.CustomizeBindingElementOrder(outgoingBindingElements, incomingBindingElements);
+
+ // Change out the standard web request handler to reflect the standard
+ // OpenID pattern that outgoing web requests are to unknown and untrusted
+ // servers on the Internet.
+ this.WebRequestHandler = new UntrustedWebRequestHandler();
+ }
+
+ /// <summary>
+ /// Verifies the integrity and applicability of an incoming message.
+ /// </summary>
+ /// <param name="message">The message just received.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the message is somehow invalid, except for check_authentication messages.
+ /// This can be due to tampering, replay attack or expiration, among other things.
+ /// </exception>
+ protected override void ProcessIncomingMessage(IProtocolMessage message) {
+ var checkAuthRequest = message as CheckAuthenticationRequest;
+ if (checkAuthRequest != null) {
+ IndirectSignedResponse originalResponse = new IndirectSignedResponse(checkAuthRequest, this);
+ try {
+ base.ProcessIncomingMessage(originalResponse);
+ checkAuthRequest.IsValid = true;
+ } catch (ProtocolException) {
+ checkAuthRequest.IsValid = false;
+ }
+ } else {
+ base.ProcessIncomingMessage(message);
+ }
+
+ // Convert an OpenID indirect error message, which we never expect
+ // between two good OpenID implementations, into an exception.
+ // We don't process DirectErrorResponse because associate negotiations
+ // commonly get a derivative of that message type and handle it.
+ var errorMessage = message as IndirectErrorResponse;
+ if (errorMessage != null) {
+ string exceptionMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ OpenIdStrings.IndirectErrorFormattedMessage,
+ errorMessage.ErrorMessage,
+ errorMessage.Contact,
+ errorMessage.Reference);
+ throw new ProtocolException(exceptionMessage, message);
+ }
+ }
+
+ /// <summary>
+ /// Prepares an HTTP request that carries a given message.
+ /// </summary>
+ /// <param name="request">The message to send.</param>
+ /// <returns>
+ /// The <see cref="HttpWebRequest"/> prepared to send the request.
+ /// </returns>
+ protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) {
+ return this.InitializeRequestAsPost(request);
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be in the given HTTP response.
+ /// </summary>
+ /// <param name="response">The response that is anticipated to contain an protocol message.</param>
+ /// <returns>
+ /// The deserialized message parts, if found. Null otherwise.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception>
+ protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) {
+ try {
+ return this.keyValueForm.GetDictionary(response.ResponseStream);
+ } catch (FormatException ex) {
+ throw ErrorUtilities.Wrap(ex, ex.Message);
+ }
+ }
+
+ /// <summary>
+ /// Called when receiving a direct response message, before deserialization begins.
+ /// </summary>
+ /// <param name="response">The HTTP direct response.</param>
+ /// <param name="message">The newly instantiated message, prior to deserialization.</param>
+ protected override void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) {
+ base.OnReceivingDirectResponse(response, message);
+
+ // Verify that the expected HTTP status code was used for the message,
+ // per OpenID 2.0 section 5.1.2.2.
+ // Note: The v1.1 spec doesn't require 400 responses for some error messages
+ if (message.Version.Major >= 2) {
+ var httpDirectResponse = message as IHttpDirectResponse;
+ if (httpDirectResponse != null) {
+ ErrorUtilities.VerifyProtocol(
+ httpDirectResponse.HttpStatusCode == response.Status,
+ MessagingStrings.UnexpectedHttpStatusCode,
+ (int)httpDirectResponse.HttpStatusCode,
+ (int)response.Status);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Queues a message for sending in the response stream where the fields
+ /// are sent in the response stream in querystring style.
+ /// </summary>
+ /// <param name="response">The message to send as a response.</param>
+ /// <returns>
+ /// The pending user agent redirect based message to be sent as an HttpResponse.
+ /// </returns>
+ /// <remarks>
+ /// This method implements spec V1.0 section 5.3.
+ /// </remarks>
+ protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) {
+ var messageAccessor = this.MessageDescriptions.GetAccessor(response);
+ var fields = messageAccessor.Serialize();
+ byte[] keyValueEncoding = KeyValueFormEncoding.GetBytes(fields);
+
+ OutgoingWebResponse preparedResponse = new OutgoingWebResponse();
+ preparedResponse.Headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType);
+ preparedResponse.OriginalMessage = response;
+ preparedResponse.ResponseStream = new MemoryStream(keyValueEncoding);
+
+ IHttpDirectResponse httpMessage = response as IHttpDirectResponse;
+ if (httpMessage != null) {
+ preparedResponse.Status = httpMessage.HttpStatusCode;
+ }
+
+ return preparedResponse;
+ }
+
+ /// <summary>
+ /// Gets the direct response of a direct HTTP request.
+ /// </summary>
+ /// <param name="webRequest">The web request.</param>
+ /// <returns>The response to the web request.</returns>
+ /// <exception cref="ProtocolException">Thrown on network or protocol errors.</exception>
+ protected override IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) {
+ IncomingWebResponse response = this.WebRequestHandler.GetResponse(webRequest, DirectWebRequestOptions.AcceptAllHttpResponses);
+
+ // Filter the responses to the allowable set of HTTP status codes.
+ if (response.Status != HttpStatusCode.OK && response.Status != HttpStatusCode.BadRequest) {
+ if (Logger.Channel.IsErrorEnabled) {
+ using (var reader = new StreamReader(response.ResponseStream)) {
+ Logger.Channel.ErrorFormat(
+ "Unexpected HTTP status code {0} {1} received in direct response:{2}{3}",
+ (int)response.Status,
+ response.Status,
+ Environment.NewLine,
+ reader.ReadToEnd());
+ }
+ }
+
+ // Call dispose before throwing since we're not including the response in the
+ // exception we're throwing.
+ response.Dispose();
+
+ ErrorUtilities.ThrowProtocol(OpenIdStrings.UnexpectedHttpStatusCode, (int)response.Status, response.Status);
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs
new file mode 100644
index 0000000..75b01e1
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs
@@ -0,0 +1,47 @@
+//-----------------------------------------------------------------------
+// <copyright file="OriginalStringUriEncoder.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A Uri encoder that serializes using <see cref="Uri.OriginalString"/>
+ /// rather than the standard <see cref="Uri.AbsoluteUri"/>.
+ /// </summary>
+ internal class OriginalStringUriEncoder : 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) {
+ Uri uriValue = (Uri)value;
+ return uriValue != null ? uriValue.OriginalString : 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) {
+ return value != null ? new Uri(value) : null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
new file mode 100644
index 0000000..25a29bb
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
@@ -0,0 +1,210 @@
+//-----------------------------------------------------------------------
+// <copyright file="ReturnToSignatureBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.Diagnostics.Contracts;
+ using System.Security.Cryptography;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// This binding element signs a Relying Party's openid.return_to parameter
+ /// so that upon return, it can verify that it hasn't been tampered with.
+ /// </summary>
+ /// <remarks>
+ /// <para>Since Providers can send unsolicited assertions, not all openid.return_to
+ /// values will be signed. But those that are signed will be validated, and
+ /// any invalid or missing signatures will cause this library to not trust
+ /// the parameters in the return_to URL.</para>
+ /// <para>In the messaging stack, this binding element looks like an ordinary
+ /// transform-type of binding element rather than a protection element,
+ /// due to its required order in the channel stack and that it doesn't sign
+ /// anything except a particular message part.</para>
+ /// </remarks>
+ internal class ReturnToSignatureBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// The name of the callback parameter we'll tack onto the return_to value
+ /// to store our signature on the return_to parameter.
+ /// </summary>
+ private const string ReturnToSignatureParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig";
+
+ /// <summary>
+ /// The name of the callback parameter we'll tack onto the return_to value
+ /// to store the handle of the association we use to sign the return_to parameter.
+ /// </summary>
+ private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle";
+
+ /// <summary>
+ /// The URI to use for private associations at this RP.
+ /// </summary>
+ private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret");
+
+ /// <summary>
+ /// The key store used to generate the private signature on the return_to parameter.
+ /// </summary>
+ private ICryptoKeyStore cryptoKeyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) {
+ Requires.NotNull(cryptoKeyStore, "cryptoKeyStore");
+
+ this.cryptoKeyStore = cryptoKeyStore;
+ }
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.None"/></value>
+ /// <remarks>
+ /// No message protection is reported because this binding element
+ /// does not protect the entire message -- only a part.
+ /// </remarks>
+ public MessageProtections Protection {
+ get { return MessageProtections.None; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ SignedResponseRequest request = message as SignedResponseRequest;
+ if (request != null && request.ReturnTo != null && request.SignReturnTo) {
+ var cryptoKeyPair = this.cryptoKeyStore.GetCurrentKey(SecretUri.AbsoluteUri, OpenIdElement.Configuration.MaxAuthenticationTime);
+ request.AddReturnToArguments(ReturnToSignatureHandleParameterName, cryptoKeyPair.Key);
+ string signature = Convert.ToBase64String(this.GetReturnToSignature(request.ReturnTo, cryptoKeyPair.Value));
+ request.AddReturnToArguments(ReturnToSignatureParameterName, signature);
+
+ // We return none because we are not signing the entire message (only a part).
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ IndirectSignedResponse response = message as IndirectSignedResponse;
+
+ if (response != null) {
+ // We can't use response.GetReturnToArgument(string) because that relies
+ // on us already having validated this signature.
+ NameValueCollection returnToParameters = HttpUtility.ParseQueryString(response.ReturnTo.Query);
+
+ // Only check the return_to signature if one is present.
+ if (returnToParameters[ReturnToSignatureHandleParameterName] != null) {
+ // Set the safety flag showing whether the return_to url had a valid signature.
+ byte[] expectedBytes = this.GetReturnToSignature(response.ReturnTo);
+ string actual = returnToParameters[ReturnToSignatureParameterName];
+ actual = OpenIdUtilities.FixDoublyUriDecodedBase64String(actual);
+ byte[] actualBytes = Convert.FromBase64String(actual);
+ response.ReturnToParametersSignatureValidated = MessagingUtilities.AreEquivalentConstantTime(actualBytes, expectedBytes);
+ if (!response.ReturnToParametersSignatureValidated) {
+ Logger.Bindings.WarnFormat("The return_to signature failed verification.");
+ }
+
+ return MessageProtections.None;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the return to signature.
+ /// </summary>
+ /// <param name="returnTo">The return to.</param>
+ /// <param name="cryptoKey">The crypto key.</param>
+ /// <returns>
+ /// The generated signature.
+ /// </returns>
+ /// <remarks>
+ /// Only the parameters in the return_to URI are signed, rather than the base URI
+ /// itself, in order that OPs that might change the return_to's implicit port :80 part
+ /// or other minor changes do not invalidate the signature.
+ /// </remarks>
+ private byte[] GetReturnToSignature(Uri returnTo, CryptoKey cryptoKey = null) {
+ Requires.NotNull(returnTo, "returnTo");
+
+ // Assemble the dictionary to sign, taking care to remove the signature itself
+ // in order to accurately reproduce the original signature (which of course didn't include
+ // the signature).
+ // Also we need to sort the dictionary's keys so that we sign in the same order as we did
+ // the last time.
+ var returnToParameters = HttpUtility.ParseQueryString(returnTo.Query);
+ returnToParameters.Remove(ReturnToSignatureParameterName);
+ var sortedReturnToParameters = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ foreach (string key in returnToParameters) {
+ sortedReturnToParameters.Add(key, returnToParameters[key]);
+ }
+
+ Logger.Bindings.DebugFormat("ReturnTo signed data: {0}{1}", Environment.NewLine, sortedReturnToParameters.ToStringDeferred());
+
+ // Sign the parameters.
+ byte[] bytesToSign = KeyValueFormEncoding.GetBytes(sortedReturnToParameters);
+ byte[] signature;
+ try {
+ if (cryptoKey == null) {
+ cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]);
+ }
+
+ using (var signer = new HMACSHA256(cryptoKey.Key)) {
+ signature = signer.ComputeHash(bytesToSign);
+ }
+ } catch (ProtocolException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired);
+ }
+
+ return signature;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs
new file mode 100644
index 0000000..d14ca8a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs
@@ -0,0 +1,199 @@
+//-----------------------------------------------------------------------
+// <copyright file="SigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net.Security;
+ using System.Web;
+ using DotNetOpenAuth.Loggers;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Signs and verifies authentication assertions.
+ /// </summary>
+ [ContractClass(typeof(SigningBindingElementContract))]
+ internal abstract class SigningBindingElement : IChannelBindingElement {
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.TamperProtection"/></value>
+ public MessageProtections Protection {
+ get { return MessageProtections.TamperProtection; }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ public Channel Channel { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets a value indicating whether this binding element is on a Provider channel.
+ /// </summary>
+ protected virtual bool IsOnProvider {
+ get { return false; }
+ }
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ public virtual MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ var signedMessage = message as ITamperResistantOpenIdMessage;
+ if (signedMessage != null) {
+ Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature);
+ MessageProtections protectionsApplied = MessageProtections.TamperProtection;
+
+ this.EnsureParametersRequiringSignatureAreSigned(signedMessage);
+
+ Association association = this.GetSpecificAssociation(signedMessage);
+ if (association != null) {
+ string signature = this.GetSignature(signedMessage, association);
+ if (!MessagingUtilities.EqualsConstantTime(signedMessage.Signature, signature)) {
+ Logger.Bindings.Error("Signature verification failed.");
+ throw new InvalidSignatureException(message);
+ }
+ } else {
+ ErrorUtilities.VerifyInternal(this.Channel != null, "Cannot verify private association signature because we don't have a channel.");
+
+ protectionsApplied = this.VerifySignatureByUnrecognizedHandle(message, signedMessage, protectionsApplied);
+ }
+
+ return protectionsApplied;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Verifies the signature by unrecognized handle.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="signedMessage">The signed message.</param>
+ /// <param name="protectionsApplied">The protections applied.</param>
+ /// <returns>The applied protections.</returns>
+ protected abstract MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied);
+
+ #endregion
+
+ /// <summary>
+ /// Calculates the signature for a given message.
+ /// </summary>
+ /// <param name="signedMessage">The message to sign or verify.</param>
+ /// <param name="association">The association to use to sign the message.</param>
+ /// <returns>The calculated signature of the method.</returns>
+ protected string GetSignature(ITamperResistantOpenIdMessage signedMessage, Association association) {
+ Requires.NotNull(signedMessage, "signedMessage");
+ Requires.True(!String.IsNullOrEmpty(signedMessage.SignedParameterOrder), "signedMessage");
+ Requires.NotNull(association, "association");
+
+ // Prepare the parts to sign, taking care to replace an openid.mode value
+ // of check_authentication with its original id_res so the signature matches.
+ MessageDictionary dictionary = this.Channel.MessageDescriptions.GetAccessor(signedMessage);
+ var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',')
+ let prefixedName = Protocol.V20.openid.Prefix + name
+ select new KeyValuePair<string, string>(name, dictionary.GetValueOrThrow(prefixedName, signedMessage));
+
+ byte[] dataToSign = KeyValueFormEncoding.GetBytes(parametersToSign);
+ string signature = Convert.ToBase64String(association.Sign(dataToSign));
+
+ if (Logger.Signatures.IsDebugEnabled) {
+ Logger.Signatures.DebugFormat(
+ "Signing these message parts: {0}{1}{0}Base64 representation of signed data: {2}{0}Signature: {3}",
+ Environment.NewLine,
+ parametersToSign.ToStringDeferred(),
+ Convert.ToBase64String(dataToSign),
+ signature);
+ }
+
+ return signature;
+ }
+
+ /// <summary>
+ /// Gets the association to use to sign or verify a message.
+ /// </summary>
+ /// <param name="signedMessage">The message to sign or verify.</param>
+ /// <returns>The association to use to sign or verify the message.</returns>
+ protected abstract Association GetAssociation(ITamperResistantOpenIdMessage signedMessage);
+
+ /// <summary>
+ /// Gets a specific association referenced in a given message's association handle.
+ /// </summary>
+ /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param>
+ /// <returns>The referenced association; or <c>null</c> if such an association cannot be found.</returns>
+ /// <remarks>
+ /// If the association handle set in the message does not match any valid association,
+ /// the association handle property is cleared, and the
+ /// <see cref="ITamperResistantOpenIdMessage.InvalidateHandle"/> property is set to the
+ /// handle that could not be found.
+ /// </remarks>
+ protected abstract Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage);
+
+ /// <summary>
+ /// Gets a private Provider association used for signing messages in "dumb" mode.
+ /// </summary>
+ /// <returns>An existing or newly created association.</returns>
+ protected virtual Association GetDumbAssociationForSigning() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Ensures that all message parameters that must be signed are in fact included
+ /// in the signature.
+ /// </summary>
+ /// <param name="signedMessage">The signed message.</param>
+ private void EnsureParametersRequiringSignatureAreSigned(ITamperResistantOpenIdMessage signedMessage) {
+ // Verify that the signed parameter order includes the mandated fields.
+ // We do this in such a way that derived classes that add mandated fields automatically
+ // get included in the list of checked parameters.
+ Protocol protocol = Protocol.Lookup(signedMessage.Version);
+ var partsRequiringProtection = from part in this.Channel.MessageDescriptions.Get(signedMessage).Mapping.Values
+ where part.RequiredProtection != ProtectionLevel.None
+ where part.IsRequired || part.IsNondefaultValueSet(signedMessage)
+ select part.Name;
+ ErrorUtilities.VerifyInternal(partsRequiringProtection.All(name => name.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)), "Signing only works when the parameters start with the 'openid.' prefix.");
+ string[] signedParts = signedMessage.SignedParameterOrder.Split(',');
+ var unsignedParts = from partName in partsRequiringProtection
+ where !signedParts.Contains(partName.Substring(protocol.openid.Prefix.Length))
+ select partName;
+ ErrorUtilities.VerifyProtocol(!unsignedParts.Any(), OpenIdStrings.SignatureDoesNotIncludeMandatoryParts, string.Join(", ", unsignedParts.ToArray()));
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs
new file mode 100644
index 0000000..bf8b18d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElementContract.cs
@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------
+// <copyright file="SigningBindingElementContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net.Security;
+ using System.Web;
+ using DotNetOpenAuth.Loggers;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Code contract for the <see cref="SigningBindingElement"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(SigningBindingElement))]
+ internal abstract class SigningBindingElementContract : SigningBindingElement {
+ /// <summary>
+ /// Verifies the signature by unrecognized handle.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="signedMessage">The signed message.</param>
+ /// <param name="protectionsApplied">The protections applied.</param>
+ /// <returns>
+ /// The applied protections.
+ /// </returns>
+ protected override MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied) {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets the association to use to sign or verify a message.
+ /// </summary>
+ /// <param name="signedMessage">The message to sign or verify.</param>
+ /// <returns>
+ /// The association to use to sign or verify the message.
+ /// </returns>
+ protected override Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) {
+ Requires.NotNull(signedMessage, "signedMessage");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets a specific association referenced in a given message's association handle.
+ /// </summary>
+ /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param>
+ /// <returns>
+ /// The referenced association; or <c>null</c> if such an association cannot be found.
+ /// </returns>
+ protected override Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) {
+ Requires.NotNull(signedMessage, "signedMessage");
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs
new file mode 100644
index 0000000..ad65a83
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------
+// <copyright file="SkipSecurityBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Spoofs security checks on incoming OpenID messages.
+ /// </summary>
+ internal class SkipSecurityBindingElement : IChannelBindingElement {
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the protection commonly offered (if any) by this binding element.
+ /// </summary>
+ /// <value><see cref="MessageProtections.All"/></value>
+ /// <remarks>
+ /// This value is used to assist in sorting binding elements in the channel stack.
+ /// </remarks>
+ public MessageProtections Protection {
+ get { return MessageProtections.All; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ Debug.Fail("SkipSecurityBindingElement.ProcessOutgoingMessage should never be called.");
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ var signedMessage = message as ITamperResistantOpenIdMessage;
+ if (signedMessage != null) {
+ Logger.Bindings.DebugFormat("Skipped security checks of incoming {0} message for preview purposes.", message.GetType().Name);
+ return this.Protection;
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}