summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
committerAndrew Arnott <andrewarnott@gmail.com>2012-01-29 14:32:45 -0800
commit5fec515095ee10b522f414a03e78f282aaf520dc (patch)
tree204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.OAuth/OAuth/ChannelElements
parentf1a4155398635a4fd9f485eec817152627682704 (diff)
parent8f4165ee515728aca3faaa26e8354a40612e85e4 (diff)
downloadDotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.zip
DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.gz
DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.bz2
Merge branch 'splitDlls'.
DNOA now builds and (in some cases) ships as many distinct assemblies.
Diffstat (limited to 'src/DotNetOpenAuth.OAuth/OAuth/ChannelElements')
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs50
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs33
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs30
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs47
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs169
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs356
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs76
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs60
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs31
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs329
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs47
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs142
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs30
-rw-r--r--src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs77
14 files changed, 1477 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs
new file mode 100644
index 0000000..5828428
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------
+// <copyright file="HmacSha1SigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Security.Cryptography;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A binding element that signs outgoing messages and verifies the signature on incoming messages.
+ /// </summary>
+ public class HmacSha1SigningBindingElement : SigningBindingElementBase {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HmacSha1SigningBindingElement"/> class
+ /// </summary>
+ public HmacSha1SigningBindingElement()
+ : base("HMAC-SHA1") {
+ }
+
+ /// <summary>
+ /// Calculates a signature for a given message.
+ /// </summary>
+ /// <param name="message">The message to sign.</param>
+ /// <returns>The signature for the message.</returns>
+ /// <remarks>
+ /// This method signs the message per OAuth 1.0 section 9.2.
+ /// </remarks>
+ protected override string GetSignature(ITamperResistantOAuthMessage message) {
+ string key = GetConsumerAndTokenSecretString(message);
+ using (HashAlgorithm hasher = new HMACSHA1(Encoding.ASCII.GetBytes(key))) {
+ string baseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message));
+ byte[] digest = hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString));
+ return Convert.ToBase64String(digest);
+ }
+ }
+
+ /// <summary>
+ /// Clones this instance.
+ /// </summary>
+ /// <returns>A new instance of the binding element.</returns>
+ protected override ITamperProtectionChannelBindingElement Clone() {
+ return new HmacSha1SigningBindingElement();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs
new file mode 100644
index 0000000..dd28e71
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------
+// <copyright file="ICombinedOpenIdProviderTokenManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using DotNetOpenAuth.OpenId;
+
+ /// <summary>
+ /// An interface that providers that play a dual role as OpenID Provider
+ /// and OAuth Service Provider should implement on their token manager classes.
+ /// </summary>
+ /// <remarks>
+ /// This interface should be implemented by the same class that implements
+ /// <see cref="ITokenManager"/> in order to enable the OpenID+OAuth extension.
+ /// </remarks>
+ public interface ICombinedOpenIdProviderTokenManager : IOpenIdOAuthTokenManager, ITokenManager {
+ /// <summary>
+ /// Gets the OAuth consumer key for a given OpenID relying party realm.
+ /// </summary>
+ /// <param name="realm">The relying party's OpenID realm.</param>
+ /// <returns>The OAuth consumer key for a given OpenID realm.</returns>
+ /// <para>This is a security-critical function. Since OpenID requests
+ /// and OAuth extensions for those requests can be formulated by ANYONE
+ /// (no signing is required by the relying party), and since the response to
+ /// the authentication will include access the user is granted to the
+ /// relying party who CLAIMS to be from some realm, it is of paramount
+ /// importance that the realm is recognized as belonging to the consumer
+ /// key by the host service provider in order to protect against phishers.</para>
+ string GetConsumerKey(Realm realm);
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs
new file mode 100644
index 0000000..b3ee320
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+// <copyright file="IOpenIdOAuthTokenManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.OpenId.Extensions.OAuth;
+
+ /// <summary>
+ /// Additional methods an <see cref="ITokenManager"/> implementing class
+ /// may implement to support the OpenID+OAuth extension.
+ /// </summary>
+ public interface IOpenIdOAuthTokenManager {
+ /// <summary>
+ /// Stores a new request token obtained over an OpenID request.
+ /// </summary>
+ /// <param name="consumerKey">The consumer key.</param>
+ /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param>
+ /// <remarks>
+ /// <para>The token secret is the empty string.</para>
+ /// <para>Tokens stored by this method should be short-lived to mitigate
+ /// possible security threats. Their lifetime should be sufficient for the
+ /// relying party to receive the positive authentication assertion and immediately
+ /// send a follow-up request for the access token.</para>
+ /// </remarks>
+ void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization);
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs
new file mode 100644
index 0000000..a95001d
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs
@@ -0,0 +1,47 @@
+//-----------------------------------------------------------------------
+// <copyright file="ITamperResistantOAuthMessage.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An interface that OAuth messages implement to support signing.
+ /// </summary>
+ public interface ITamperResistantOAuthMessage : IDirectedProtocolMessage, ITamperResistantProtocolMessage, IMessageOriginalPayload {
+ /// <summary>
+ /// Gets or sets the method used to sign the message.
+ /// </summary>
+ string SignatureMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Token Secret used to sign the message.
+ /// </summary>
+ string TokenSecret { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Consumer key.
+ /// </summary>
+ string ConsumerKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Consumer Secret used to sign the message.
+ /// </summary>
+ string ConsumerSecret { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP method that will be used to transmit the message.
+ /// </summary>
+ string HttpMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL of the intended receiver of this message.
+ /// </summary>
+ new Uri Recipient { get; set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs
new file mode 100644
index 0000000..7d68b63
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs
@@ -0,0 +1,169 @@
+//-----------------------------------------------------------------------
+// <copyright file="ITokenManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.OAuth.Messages;
+
+ /// <summary>
+ /// An interface OAuth hosts must implement for persistent storage
+ /// and recall of tokens and secrets for an individual OAuth consumer
+ /// or service provider.
+ /// </summary>
+ [ContractClass(typeof(ITokenManagerContract))]
+ public interface ITokenManager {
+ /// <summary>
+ /// Gets the Token Secret given a request or access token.
+ /// </summary>
+ /// <param name="token">The request or access token.</param>
+ /// <returns>The secret associated with the given token.</returns>
+ /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception>
+ string GetTokenSecret(string token);
+
+ /// <summary>
+ /// Stores a newly generated unauthorized request token, secret, and optional
+ /// application-specific parameters for later recall.
+ /// </summary>
+ /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param>
+ /// <param name="response">The response message that includes the unauthorized request token.</param>
+ /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception>
+ /// <remarks>
+ /// Request tokens stored by this method SHOULD NOT associate any user account with this token.
+ /// It usually opens up security holes in your application to do so. Instead, you associate a user
+ /// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/>
+ /// method.
+ /// </remarks>
+ void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response);
+
+ /// <summary>
+ /// Deletes a request token and its associated secret and stores a new access token and secret.
+ /// </summary>
+ /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param>
+ /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param>
+ /// <param name="accessToken">The new access token that is being issued to the Consumer.</param>
+ /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param>
+ /// <remarks>
+ /// <para>
+ /// Any scope of granted privileges associated with the request token from the
+ /// original call to <see cref="StoreNewRequestToken"/> should be carried over
+ /// to the new Access Token.
+ /// </para>
+ /// <para>
+ /// To associate a user account with the new access token,
+ /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be
+ /// useful in an ASP.NET web application within the implementation of this method.
+ /// Alternatively you may store the access token here without associating with a user account,
+ /// and wait until WebConsumer.ProcessUserAuthorization or
+ /// DesktopConsumer.ProcessUserAuthorization return the access
+ /// token to associate the access token with a user account at that point.
+ /// </para>
+ /// </remarks>
+ void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret);
+
+ /// <summary>
+ /// Classifies a token as a request token or an access token.
+ /// </summary>
+ /// <param name="token">The token to classify.</param>
+ /// <returns>Request or Access token, or invalid if the token is not recognized.</returns>
+ TokenType GetTokenType(string token);
+ }
+
+ /// <summary>
+ /// The code contract class for the <see cref="ITokenManager"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(ITokenManager))]
+ internal abstract class ITokenManagerContract : ITokenManager {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="ITokenManagerContract"/> class from being created.
+ /// </summary>
+ private ITokenManagerContract() {
+ }
+
+ #region ITokenManager Members
+
+ /// <summary>
+ /// Gets the Token Secret given a request or access token.
+ /// </summary>
+ /// <param name="token">The request or access token.</param>
+ /// <returns>
+ /// The secret associated with the given token.
+ /// </returns>
+ /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception>
+ string ITokenManager.GetTokenSecret(string token) {
+ Requires.NotNullOrEmpty(token, "token");
+ Contract.Ensures(Contract.Result<string>() != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Stores a newly generated unauthorized request token, secret, and optional
+ /// application-specific parameters for later recall.
+ /// </summary>
+ /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param>
+ /// <param name="response">The response message that includes the unauthorized request token.</param>
+ /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception>
+ /// <remarks>
+ /// Request tokens stored by this method SHOULD NOT associate any user account with this token.
+ /// It usually opens up security holes in your application to do so. Instead, you associate a user
+ /// account with access tokens (not request tokens) in the <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/>
+ /// method.
+ /// </remarks>
+ void ITokenManager.StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) {
+ Requires.NotNull(request, "request");
+ Requires.NotNull(response, "response");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Deletes a request token and its associated secret and stores a new access token and secret.
+ /// </summary>
+ /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param>
+ /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param>
+ /// <param name="accessToken">The new access token that is being issued to the Consumer.</param>
+ /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param>
+ /// <remarks>
+ /// <para>
+ /// Any scope of granted privileges associated with the request token from the
+ /// original call to <see cref="ITokenManager.StoreNewRequestToken"/> should be carried over
+ /// to the new Access Token.
+ /// </para>
+ /// <para>
+ /// To associate a user account with the new access token,
+ /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be
+ /// useful in an ASP.NET web application within the implementation of this method.
+ /// Alternatively you may store the access token here without associating with a user account,
+ /// and wait until WebConsumer.ProcessUserAuthorization or
+ /// DesktopConsumer.ProcessUserAuthorization return the access
+ /// token to associate the access token with a user account at that point.
+ /// </para>
+ /// </remarks>
+ void ITokenManager.ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) {
+ Requires.NotNullOrEmpty(consumerKey, "consumerKey");
+ Requires.NotNullOrEmpty(requestToken, "requestToken");
+ Requires.NotNullOrEmpty(accessToken, "accessToken");
+ Requires.NotNull(accessTokenSecret, "accessTokenSecret");
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Classifies a token as a request token or an access token.
+ /// </summary>
+ /// <param name="token">The token to classify.</param>
+ /// <returns>
+ /// Request or Access token, or invalid if the token is not recognized.
+ /// </returns>
+ TokenType ITokenManager.GetTokenType(string token) {
+ Requires.NotNullOrEmpty(token, "token");
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs
new file mode 100644
index 0000000..32b57d0
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs
@@ -0,0 +1,356 @@
+//-----------------------------------------------------------------------
+// <copyright file="OAuthChannel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OAuth.Messages;
+
+ /// <summary>
+ /// An OAuth-specific implementation of the <see cref="Channel"/> class.
+ /// </summary>
+ internal abstract class OAuthChannel : Channel {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuthChannel"/> class.
+ /// </summary>
+ /// <param name="signingBindingElement">The binding element to use for signing.</param>
+ /// <param name="store">The web application store to use for nonces.</param>
+ /// <param name="tokenManager">The ITokenManager instance to use.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ /// <param name="messageTypeProvider">An injected message type provider instance.
+ /// Except for mock testing, this should always be one of
+ /// OAuthConsumerMessageFactory or OAuthServiceProviderMessageFactory.</param>
+ /// <param name="bindingElements">The binding elements.</param>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")]
+ protected OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings, IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements)
+ : base(messageTypeProvider, bindingElements) {
+ Requires.NotNull(tokenManager, "tokenManager");
+ Requires.NotNull(securitySettings, "securitySettings");
+ Requires.NotNull(signingBindingElement, "signingBindingElement");
+ Requires.True(signingBindingElement.SignatureCallback == null, "signingBindingElement", OAuthStrings.SigningElementAlreadyAssociatedWithChannel);
+ Requires.NotNull(bindingElements, "bindingElements");
+
+ this.TokenManager = tokenManager;
+ signingBindingElement.SignatureCallback = this.SignatureCallback;
+ }
+
+ /// <summary>
+ /// Gets or sets the Consumer web application path.
+ /// </summary>
+ internal Uri Realm { get; set; }
+
+ /// <summary>
+ /// Gets the token manager being used.
+ /// </summary>
+ protected internal ITokenManager TokenManager { get; private set; }
+
+ /// <summary>
+ /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1.
+ /// </summary>
+ /// <param name="message">The message with data to encode.</param>
+ /// <returns>A dictionary of name-value pairs with their strings encoded.</returns>
+ internal static IDictionary<string, string> GetUriEscapedParameters(IEnumerable<KeyValuePair<string, string>> message) {
+ var encodedDictionary = new Dictionary<string, string>();
+ UriEscapeParameters(message, encodedDictionary);
+ return encodedDictionary;
+ }
+
+ /// <summary>
+ /// Initializes a web request for sending by attaching a message to it.
+ /// Use this method to prepare a protected resource request that you do NOT
+ /// expect an OAuth message response to.
+ /// </summary>
+ /// <param name="request">The message to attach.</param>
+ /// <returns>The initialized web request.</returns>
+ internal HttpWebRequest InitializeRequest(IDirectedProtocolMessage request) {
+ Requires.NotNull(request, "request");
+
+ ProcessOutgoingMessage(request);
+ return this.CreateHttpRequest(request);
+ }
+
+ /// <summary>
+ /// Initializes the binding elements for the OAuth channel.
+ /// </summary>
+ /// <param name="signingBindingElement">The signing binding element.</param>
+ /// <param name="store">The nonce store.</param>
+ /// <param name="tokenManager">The token manager.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ /// <returns>
+ /// An array of binding elements used to initialize the channel.
+ /// </returns>
+ protected static List<IChannelBindingElement> InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings) {
+ Contract.Requires(securitySettings != null);
+
+ var bindingElements = new List<IChannelBindingElement> {
+ new OAuthHttpMethodBindingElement(),
+ signingBindingElement,
+ new StandardExpirationBindingElement(),
+ new StandardReplayProtectionBindingElement(store),
+ };
+
+ return bindingElements;
+ }
+
+ /// <summary>
+ /// Searches an incoming HTTP request for data that could be used to assemble
+ /// a protocol request message.
+ /// </summary>
+ /// <param name="request">The HTTP request to search.</param>
+ /// <returns>The deserialized message, if one is found. Null otherwise.</returns>
+ protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) {
+ // First search the Authorization header.
+ string authorization = request.Headers[HttpRequestHeader.Authorization];
+ var fields = MessagingUtilities.ParseAuthorizationHeader(Protocol.AuthorizationHeaderScheme, authorization).ToDictionary();
+ fields.Remove("realm"); // ignore the realm parameter, since we don't use it, and it must be omitted from signature base string.
+
+ // Scrape the entity
+ if (!string.IsNullOrEmpty(request.Headers[HttpRequestHeader.ContentType])) {
+ var contentType = new ContentType(request.Headers[HttpRequestHeader.ContentType]);
+ if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) {
+ foreach (string key in request.Form) {
+ if (key != null) {
+ fields.Add(key, request.Form[key]);
+ } else {
+ Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.Form[key]);
+ }
+ }
+ }
+ }
+
+ // Scrape the query string
+ foreach (string key in request.QueryStringBeforeRewriting) {
+ if (key != null) {
+ fields.Add(key, request.QueryStringBeforeRewriting[key]);
+ } else {
+ Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.QueryStringBeforeRewriting[key]);
+ }
+ }
+
+ MessageReceivingEndpoint recipient;
+ try {
+ recipient = request.GetRecipient();
+ } catch (ArgumentException ex) {
+ Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString());
+ return null;
+ }
+
+ // Deserialize the message using all the data we've collected.
+ var message = (IDirectedProtocolMessage)this.Receive(fields, recipient);
+
+ // Add receiving HTTP transport information required for signature generation.
+ var signedMessage = message as ITamperResistantOAuthMessage;
+ if (signedMessage != null) {
+ signedMessage.Recipient = request.UrlBeforeRewriting;
+ signedMessage.HttpMethod = request.HttpMethod;
+ }
+
+ return message;
+ }
+
+ /// <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>
+ protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) {
+ string body = response.GetResponseReader().ReadToEnd();
+ return HttpUtility.ParseQueryString(body).ToDictionary();
+ }
+
+ /// <summary>
+ /// Prepares an HTTP request that carries a given message.
+ /// </summary>
+ /// <param name="request">The message to send.</param>
+ /// <returns>
+ /// The <see cref="HttpRequest"/> prepared to send the request.
+ /// </returns>
+ protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) {
+ HttpWebRequest httpRequest;
+
+ HttpDeliveryMethods transmissionMethod = request.HttpMethods;
+ if ((transmissionMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) {
+ httpRequest = this.InitializeRequestAsAuthHeader(request);
+ } else if ((transmissionMethod & HttpDeliveryMethods.PostRequest) != 0) {
+ var requestMessageWithBinaryData = request as IMessageWithBinaryData;
+ ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || !requestMessageWithBinaryData.SendAsMultipart, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader);
+ httpRequest = this.InitializeRequestAsPost(request);
+ } else if ((transmissionMethod & HttpDeliveryMethods.GetRequest) != 0) {
+ httpRequest = InitializeRequestAsGet(request);
+ } else if ((transmissionMethod & HttpDeliveryMethods.HeadRequest) != 0) {
+ httpRequest = InitializeRequestAsHead(request);
+ } else if ((transmissionMethod & HttpDeliveryMethods.PutRequest) != 0) {
+ httpRequest = this.InitializeRequestAsPut(request);
+ } else if ((transmissionMethod & HttpDeliveryMethods.DeleteRequest) != 0) {
+ httpRequest = InitializeRequestAsDelete(request);
+ } else {
+ throw new NotSupportedException();
+ }
+ return httpRequest;
+ }
+
+ /// <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();
+ string responseBody = MessagingUtilities.CreateQueryString(fields);
+
+ OutgoingWebResponse encodedResponse = new OutgoingWebResponse {
+ Body = responseBody,
+ OriginalMessage = response,
+ Status = HttpStatusCode.OK,
+ Headers = new System.Net.WebHeaderCollection(),
+ };
+
+ IHttpDirectResponse httpMessage = response as IHttpDirectResponse;
+ if (httpMessage != null) {
+ encodedResponse.Status = httpMessage.HttpStatusCode;
+ }
+
+ return encodedResponse;
+ }
+
+ /// <summary>
+ /// Gets the consumer secret for a given consumer key.
+ /// </summary>
+ /// <param name="consumerKey">The consumer key.</param>
+ /// <returns>A consumer secret.</returns>
+ protected abstract string GetConsumerSecret(string consumerKey);
+
+ /// <summary>
+ /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1.
+ /// </summary>
+ /// <param name="source">The dictionary with names and values to encode.</param>
+ /// <param name="destination">The dictionary to add the encoded pairs to.</param>
+ private static void UriEscapeParameters(IEnumerable<KeyValuePair<string, string>> source, IDictionary<string, string> destination) {
+ Requires.NotNull(source, "source");
+ Requires.NotNull(destination, "destination");
+
+ foreach (var pair in source) {
+ var key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key);
+ var value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value);
+ destination.Add(key, value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the HTTP method to use for a message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>"POST", "GET" or some other similar http verb.</returns>
+ private static string GetHttpMethod(IDirectedProtocolMessage message) {
+ Requires.NotNull(message, "message");
+
+ var signedMessage = message as ITamperResistantOAuthMessage;
+ if (signedMessage != null) {
+ return signedMessage.HttpMethod;
+ } else {
+ return MessagingUtilities.GetHttpVerb(message.HttpMethods);
+ }
+ }
+
+ /// <summary>
+ /// Prepares to send a request to the Service Provider via the Authorization header.
+ /// </summary>
+ /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param>
+ /// <returns>The web request ready to send.</returns>
+ /// <remarks>
+ /// <para>If the message has non-empty ExtraData in it, the request stream is sent to
+ /// the server automatically. If it is empty, the request stream must be sent by the caller.</para>
+ /// <para>This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4).</para>
+ /// </remarks>
+ private HttpWebRequest InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) {
+ var dictionary = this.MessageDescriptions.GetAccessor(requestMessage);
+
+ // copy so as to not modify original
+ var fields = new Dictionary<string, string>();
+ foreach (string key in dictionary.DeclaredKeys) {
+ fields.Add(key, dictionary[key]);
+ }
+ if (this.Realm != null) {
+ fields.Add("realm", this.Realm.AbsoluteUri);
+ }
+
+ HttpWebRequest httpRequest;
+ UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient);
+ bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage));
+
+ if (!hasEntity) {
+ MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData);
+ }
+ httpRequest = (HttpWebRequest)WebRequest.Create(recipientBuilder.Uri);
+ httpRequest.Method = GetHttpMethod(requestMessage);
+
+ httpRequest.Headers.Add(HttpRequestHeader.Authorization, MessagingUtilities.AssembleAuthorizationHeader(Protocol.AuthorizationHeaderScheme, fields));
+
+ if (hasEntity) {
+ // WARNING: We only set up the request stream for the caller if there is
+ // extra data. If there isn't any extra data, the caller must do this themselves.
+ var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData;
+ if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) {
+ // Include the binary data in the multipart entity, and any standard text extra message data.
+ // The standard declared message parts are included in the authorization header.
+ var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData);
+ multiPartFields.AddRange(requestMessage.ExtraData.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value)));
+ this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields);
+ } else {
+ ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart);
+ if (requestMessage.ExtraData.Count > 0) {
+ this.SendParametersInEntity(httpRequest, requestMessage.ExtraData);
+ } else {
+ // We'll assume the content length is zero since the caller may not have
+ // anything. They're responsible to change it when the add the payload if they have one.
+ httpRequest.ContentLength = 0;
+ }
+ }
+ }
+
+ return httpRequest;
+ }
+
+ /// <summary>
+ /// Fills out the secrets in a message so that signing/verification can be performed.
+ /// </summary>
+ /// <param name="message">The message about to be signed or whose signature is about to be verified.</param>
+ private void SignatureCallback(ITamperResistantProtocolMessage message) {
+ var oauthMessage = message as ITamperResistantOAuthMessage;
+ try {
+ Logger.Channel.Debug("Applying secrets to message to prepare for signing or signature verification.");
+ oauthMessage.ConsumerSecret = this.GetConsumerSecret(oauthMessage.ConsumerKey);
+
+ var tokenMessage = message as ITokenContainingMessage;
+ if (tokenMessage != null) {
+ oauthMessage.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token);
+ }
+ } catch (KeyNotFoundException ex) {
+ throw new ProtocolException(OAuthStrings.ConsumerOrTokenSecretNotFound, ex);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs
new file mode 100644
index 0000000..37fb80b
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------
+// <copyright file="OAuthHttpMethodBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Sets the HTTP Method property on a signed message before the signing module gets to it.
+ /// </summary>
+ internal class OAuthHttpMethodBindingElement : IChannelBindingElement {
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ public MessageProtections Protection {
+ get { return MessageProtections.None; }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ public Channel Channel { get; set; }
+
+ /// <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>
+ /// True if the <paramref name="message"/> applied to this binding element
+ /// and the operation was successful. False otherwise.
+ /// </returns>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ var oauthMessage = message as ITamperResistantOAuthMessage;
+
+ if (oauthMessage != null) {
+ HttpDeliveryMethods transmissionMethod = oauthMessage.HttpMethods;
+ try {
+ oauthMessage.HttpMethod = MessagingUtilities.GetHttpVerb(transmissionMethod);
+ return MessageProtections.None;
+ } catch (ArgumentException ex) {
+ Logger.OAuth.Error("Unrecognized HttpDeliveryMethods value.", ex);
+ return null;
+ }
+ } else {
+ 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>
+ /// True if the <paramref name="message"/> applied to this binding element
+ /// and the operation was successful. False if the operation did not apply to this message.
+ /// </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) {
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs
new file mode 100644
index 0000000..22e5f20
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs
@@ -0,0 +1,60 @@
+//-----------------------------------------------------------------------
+// <copyright file="PlaintextSigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// A binding element that signs outgoing messages and verifies the signature on incoming messages.
+ /// </summary>
+ public class PlaintextSigningBindingElement : SigningBindingElementBase {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaintextSigningBindingElement"/> class.
+ /// </summary>
+ public PlaintextSigningBindingElement()
+ : base("PLAINTEXT") {
+ }
+
+ /// <summary>
+ /// Calculates a signature for a given message.
+ /// </summary>
+ /// <param name="message">The message to sign.</param>
+ /// <returns>The signature for the message.</returns>
+ /// <remarks>
+ /// This method signs the message according to OAuth 1.0 section 9.4.1.
+ /// </remarks>
+ protected override string GetSignature(ITamperResistantOAuthMessage message) {
+ return GetConsumerAndTokenSecretString(message);
+ }
+
+ /// <summary>
+ /// Checks whether this binding element applies to this message.
+ /// </summary>
+ /// <param name="message">The message that needs to be signed.</param>
+ /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns>
+ protected override bool IsMessageApplicable(ITamperResistantOAuthMessage message) {
+ if (string.Equals(message.Recipient.Scheme, "https", StringComparison.OrdinalIgnoreCase)) {
+ return true;
+ } else {
+ Logger.Bindings.DebugFormat("The {0} element will not sign this message because the URI scheme is not https.", this.GetType().Name);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Clones this instance.
+ /// </summary>
+ /// <returns>A new instance of the binding element.</returns>
+ protected override ITamperProtectionChannelBindingElement Clone() {
+ return new PlaintextSigningBindingElement();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs
new file mode 100644
index 0000000..83be094
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------
+// <copyright file="RsaSha1SigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A binding element that signs outgoing messages and verifies the signature on incoming messages.
+ /// </summary>
+ public abstract class RsaSha1SigningBindingElement : SigningBindingElementBase {
+ /// <summary>
+ /// The name of the hash algorithm to use.
+ /// </summary>
+ protected const string HashAlgorithmName = "RSA-SHA1";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class.
+ /// </summary>
+ protected RsaSha1SigningBindingElement()
+ : base(HashAlgorithmName) {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs
new file mode 100644
index 0000000..2c47453
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs
@@ -0,0 +1,329 @@
+//-----------------------------------------------------------------------
+// <copyright file="SigningBindingElementBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// A binding element that signs outgoing messages and verifies the signature on incoming messages.
+ /// </summary>
+ [ContractClass(typeof(SigningBindingElementBaseContract))]
+ public abstract class SigningBindingElementBase : ITamperProtectionChannelBindingElement {
+ /// <summary>
+ /// The signature method this binding element uses.
+ /// </summary>
+ private string signatureMethod;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SigningBindingElementBase"/> class.
+ /// </summary>
+ /// <param name="signatureMethod">The OAuth signature method that the binding element uses.</param>
+ internal SigningBindingElementBase(string signatureMethod) {
+ this.signatureMethod = signatureMethod;
+ }
+
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets the message protection provided by this binding element.
+ /// </summary>
+ 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
+
+ #region ITamperProtectionChannelBindingElement members
+
+ /// <summary>
+ /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed
+ /// message so that its signature can be correctly calculated for verification.
+ /// </summary>
+ public Action<ITamperResistantOAuthMessage> SignatureCallback { get; set; }
+
+ /// <summary>
+ /// Creates a new object that is a copy of the current instance.
+ /// </summary>
+ /// <returns>
+ /// A new object that is a copy of this instance.
+ /// </returns>
+ ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() {
+ ITamperProtectionChannelBindingElement clone = this.Clone();
+ clone.SignatureCallback = this.SignatureCallback;
+ return clone;
+ }
+
+ #endregion
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Signs the outgoing message.
+ /// </summary>
+ /// <param name="message">The message to sign.</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 MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ var signedMessage = message as ITamperResistantOAuthMessage;
+ if (signedMessage != null && this.IsMessageApplicable(signedMessage)) {
+ if (this.SignatureCallback != null) {
+ this.SignatureCallback(signedMessage);
+ } else {
+ Logger.Bindings.Warn("Signing required, but callback delegate was not provided to provide additional data for signing.");
+ }
+
+ signedMessage.SignatureMethod = this.signatureMethod;
+ Logger.Bindings.DebugFormat("Signing {0} message using {1}.", message.GetType().Name, this.signatureMethod);
+ signedMessage.Signature = this.GetSignature(signedMessage);
+ return MessageProtections.TamperProtection;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Verifies the signature on an incoming message.
+ /// </summary>
+ /// <param name="message">The message whose signature should be verified.</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="InvalidSignatureException">Thrown if the signature is invalid.</exception>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ var signedMessage = message as ITamperResistantOAuthMessage;
+ if (signedMessage != null && this.IsMessageApplicable(signedMessage)) {
+ Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature);
+
+ if (!string.Equals(signedMessage.SignatureMethod, this.signatureMethod, StringComparison.Ordinal)) {
+ Logger.Bindings.WarnFormat("Expected signature method '{0}' but received message with a signature method of '{1}'.", this.signatureMethod, signedMessage.SignatureMethod);
+ return MessageProtections.None;
+ }
+
+ if (this.SignatureCallback != null) {
+ this.SignatureCallback(signedMessage);
+ } else {
+ Logger.Bindings.Warn("Signature verification required, but callback delegate was not provided to provide additional data for signature verification.");
+ }
+
+ if (!this.IsSignatureValid(signedMessage)) {
+ Logger.Bindings.Error("Signature verification failed.");
+ throw new InvalidSignatureException(message);
+ }
+
+ return MessageProtections.TamperProtection;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Constructs the OAuth Signature Base String and returns the result.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="messageDictionary">The message to derive the signature base string from.</param>
+ /// <returns>The signature base string.</returns>
+ /// <remarks>
+ /// This method implements OAuth 1.0 section 9.1.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
+ internal static string ConstructSignatureBaseString(ITamperResistantOAuthMessage message, MessageDictionary messageDictionary) {
+ Requires.NotNull(message, "message");
+ Requires.NotNullOrEmpty(message.HttpMethod, "message.HttpMethod");
+ Requires.NotNull(messageDictionary, "messageDictionary");
+ ErrorUtilities.VerifyArgument(messageDictionary.Message == message, "Message references are not equal.");
+
+ List<string> signatureBaseStringElements = new List<string>(3);
+
+ signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant());
+
+ // For multipart POST messages, only include the message parts that are NOT
+ // in the POST entity (those parts that may appear in an OAuth authorization header).
+ var encodedDictionary = new Dictionary<string, string>();
+ IEnumerable<KeyValuePair<string, string>> partsToInclude = Enumerable.Empty<KeyValuePair<string, string>>();
+ var binaryMessage = message as IMessageWithBinaryData;
+ if (binaryMessage != null && binaryMessage.SendAsMultipart) {
+ HttpDeliveryMethods authHeaderInUseFlags = HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest;
+ ErrorUtilities.VerifyProtocol((binaryMessage.HttpMethods & authHeaderInUseFlags) == authHeaderInUseFlags, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader);
+
+ // Include the declared keys in the signature as those will be signable.
+ // Cache in local variable to avoid recalculating DeclaredKeys in the delegate.
+ ICollection<string> declaredKeys = messageDictionary.DeclaredKeys;
+ partsToInclude = messageDictionary.Where(pair => declaredKeys.Contains(pair.Key));
+ } else {
+ partsToInclude = messageDictionary;
+ }
+
+ // If this message was deserialized, include only those explicitly included message parts (excludes defaulted values)
+ // in the signature.
+ var originalPayloadMessage = (IMessageOriginalPayload)message;
+ if (originalPayloadMessage.OriginalPayload != null) {
+ partsToInclude = partsToInclude.Where(pair => originalPayloadMessage.OriginalPayload.ContainsKey(pair.Key));
+ }
+
+ foreach (var pair in OAuthChannel.GetUriEscapedParameters(partsToInclude)) {
+ encodedDictionary[pair.Key] = pair.Value;
+ }
+
+ // An incoming message will already have included the query and form parameters
+ // in the message dictionary, but an outgoing message COULD have SOME parameters
+ // in the query that are not in the message dictionary because they were included
+ // in the receiving endpoint (the original URL).
+ // In an outgoing message, the POST entity can only contain parameters if they were
+ // in the message dictionary, so no need to pull out any parameters from there.
+ if (message.Recipient.Query != null) {
+ NameValueCollection nvc = HttpUtility.ParseQueryString(message.Recipient.Query);
+ foreach (string key in nvc) {
+ string escapedKey = MessagingUtilities.EscapeUriDataStringRfc3986(key);
+ string escapedValue = MessagingUtilities.EscapeUriDataStringRfc3986(nvc[key]);
+ string existingValue;
+ if (!encodedDictionary.TryGetValue(escapedKey, out existingValue)) {
+ encodedDictionary.Add(escapedKey, escapedValue);
+ } else {
+ ErrorUtilities.VerifyInternal(escapedValue == existingValue, "Somehow we have conflicting values for the '{0}' parameter.", escapedKey);
+ }
+ }
+ }
+ encodedDictionary.Remove("oauth_signature");
+
+ UriBuilder endpoint = new UriBuilder(message.Recipient);
+ endpoint.Query = null;
+ endpoint.Fragment = null;
+ signatureBaseStringElements.Add(endpoint.Uri.AbsoluteUri);
+
+ var sortedKeyValueList = new List<KeyValuePair<string, string>>(encodedDictionary);
+ sortedKeyValueList.Sort(SignatureBaseStringParameterComparer);
+ StringBuilder paramBuilder = new StringBuilder();
+ foreach (var pair in sortedKeyValueList) {
+ if (paramBuilder.Length > 0) {
+ paramBuilder.Append("&");
+ }
+
+ paramBuilder.Append(pair.Key);
+ paramBuilder.Append('=');
+ paramBuilder.Append(pair.Value);
+ }
+
+ signatureBaseStringElements.Add(paramBuilder.ToString());
+
+ StringBuilder signatureBaseString = new StringBuilder();
+ foreach (string element in signatureBaseStringElements) {
+ if (signatureBaseString.Length > 0) {
+ signatureBaseString.Append("&");
+ }
+
+ signatureBaseString.Append(MessagingUtilities.EscapeUriDataStringRfc3986(element));
+ }
+
+ Logger.Bindings.DebugFormat("Constructed signature base string: {0}", signatureBaseString);
+ return signatureBaseString.ToString();
+ }
+
+ /// <summary>
+ /// Calculates a signature for a given message.
+ /// </summary>
+ /// <param name="message">The message to sign.</param>
+ /// <returns>The signature for the message.</returns>
+ /// <remarks>
+ /// This method signs the message per OAuth 1.0 section 9.2.
+ /// </remarks>
+ internal string GetSignatureTestHook(ITamperResistantOAuthMessage message) {
+ return this.GetSignature(message);
+ }
+
+ /// <summary>
+ /// Gets the "ConsumerSecret&amp;TokenSecret" string, allowing either property to be empty or null.
+ /// </summary>
+ /// <param name="message">The message to extract the secrets from.</param>
+ /// <returns>The concatenated string.</returns>
+ protected static string GetConsumerAndTokenSecretString(ITamperResistantOAuthMessage message) {
+ StringBuilder builder = new StringBuilder();
+ if (!string.IsNullOrEmpty(message.ConsumerSecret)) {
+ builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.ConsumerSecret));
+ }
+ builder.Append("&");
+ if (!string.IsNullOrEmpty(message.TokenSecret)) {
+ builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.TokenSecret));
+ }
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Determines whether the signature on some message is valid.
+ /// </summary>
+ /// <param name="message">The message to check the signature on.</param>
+ /// <returns>
+ /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>.
+ /// </returns>
+ protected virtual bool IsSignatureValid(ITamperResistantOAuthMessage message) {
+ Requires.NotNull(message, "message");
+
+ string signature = this.GetSignature(message);
+ return MessagingUtilities.EqualsConstantTime(message.Signature, signature);
+ }
+
+ /// <summary>
+ /// Clones this instance.
+ /// </summary>
+ /// <returns>A new instance of the binding element.</returns>
+ /// <remarks>
+ /// Implementations of this method need not clone the SignatureVerificationCallback member, as the
+ /// <see cref="SigningBindingElementBase"/> class does this.
+ /// </remarks>
+ protected abstract ITamperProtectionChannelBindingElement Clone();
+
+ /// <summary>
+ /// Calculates a signature for a given message.
+ /// </summary>
+ /// <param name="message">The message to sign.</param>
+ /// <returns>The signature for the message.</returns>
+ protected abstract string GetSignature(ITamperResistantOAuthMessage message);
+
+ /// <summary>
+ /// Checks whether this binding element applies to this message.
+ /// </summary>
+ /// <param name="message">The message that needs to be signed.</param>
+ /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns>
+ protected virtual bool IsMessageApplicable(ITamperResistantOAuthMessage message) {
+ return string.IsNullOrEmpty(message.SignatureMethod) || message.SignatureMethod == this.signatureMethod;
+ }
+
+ /// <summary>
+ /// Sorts parameters according to OAuth signature base string rules.
+ /// </summary>
+ /// <param name="left">The first parameter to compare.</param>
+ /// <param name="right">The second parameter to compare.</param>
+ /// <returns>Negative, zero or positive.</returns>
+ private static int SignatureBaseStringParameterComparer(KeyValuePair<string, string> left, KeyValuePair<string, string> right) {
+ int result = string.CompareOrdinal(left.Key, right.Key);
+ if (result != 0) {
+ return result;
+ }
+
+ return string.CompareOrdinal(left.Value, right.Value);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs
new file mode 100644
index 0000000..4450fb5
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs
@@ -0,0 +1,47 @@
+//-----------------------------------------------------------------------
+// <copyright file="SigningBindingElementBaseContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Code Contract for the <see cref="SigningBindingElementBase"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(SigningBindingElementBase))]
+ internal abstract class SigningBindingElementBaseContract : SigningBindingElementBase {
+ /// <summary>
+ /// Prevents a default instance of the SigningBindingElementBaseContract class from being created.
+ /// </summary>
+ private SigningBindingElementBaseContract()
+ : base(string.Empty) {
+ }
+
+ /// <summary>
+ /// Clones this instance.
+ /// </summary>
+ /// <returns>A new instance of the binding element.</returns>
+ /// <remarks>
+ /// Implementations of this method need not clone the SignatureVerificationCallback member, as the
+ /// <see cref="SigningBindingElementBase"/> class does this.
+ /// </remarks>
+ protected override ITamperProtectionChannelBindingElement Clone() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Calculates a signature for a given message.
+ /// </summary>
+ /// <param name="message">The message to sign.</param>
+ /// <returns>The signature for the message.</returns>
+ protected override string GetSignature(ITamperResistantOAuthMessage message) {
+ Requires.NotNull(message, "message");
+ Requires.ValidState(this.Channel != null);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs
new file mode 100644
index 0000000..849ad5e
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs
@@ -0,0 +1,142 @@
+//-----------------------------------------------------------------------
+// <copyright file="SigningBindingElementChain.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A tamper protection applying binding element that can use any of several given
+ /// binding elements to apply the protection.
+ /// </summary>
+ internal class SigningBindingElementChain : ITamperProtectionChannelBindingElement {
+ /// <summary>
+ /// The various signing binding elements that may be applicable to a message in preferred use order.
+ /// </summary>
+ private readonly ITamperProtectionChannelBindingElement[] signers;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SigningBindingElementChain"/> class.
+ /// </summary>
+ /// <param name="signers">
+ /// The signing binding elements that may be used for some outgoing message,
+ /// in preferred use order.
+ /// </param>
+ internal SigningBindingElementChain(ITamperProtectionChannelBindingElement[] signers) {
+ Requires.NotNullOrEmpty(signers, "signers");
+ Requires.NullOrWithNoNullElements(signers, "signers");
+ Requires.True(signers.Select(s => s.Protection).Distinct().Count() == 1, "signers", OAuthStrings.SigningElementsMustShareSameProtection);
+
+ this.signers = signers;
+ }
+
+ #region ITamperProtectionChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed
+ /// message so that its signature can be correctly calculated for verification.
+ /// May be null for Consumers (who never have to verify signatures).
+ /// </summary>
+ public Action<ITamperResistantOAuthMessage> SignatureCallback {
+ get {
+ return this.signers[0].SignatureCallback;
+ }
+
+ set {
+ foreach (ITamperProtectionChannelBindingElement signer in this.signers) {
+ signer.SignatureCallback = value;
+ }
+ }
+ }
+
+ #endregion
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ public MessageProtections Protection {
+ get { return this.signers[0].Protection; }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ public Channel Channel {
+ get {
+ return this.signers[0].Channel;
+ }
+
+ set {
+ foreach (var signer in this.signers) {
+ signer.Channel = value;
+ }
+ }
+ }
+
+ /// <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 MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ foreach (IChannelBindingElement signer in this.signers) {
+ ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null.");
+ MessageProtections? result = signer.ProcessOutgoingMessage(message);
+ if (result.HasValue) {
+ return result;
+ }
+ }
+
+ 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>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ foreach (IChannelBindingElement signer in this.signers) {
+ ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null.");
+ MessageProtections? result = signer.ProcessIncomingMessage(message);
+ if (result.HasValue) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region ITamperProtectionChannelBindingElement Methods
+
+ /// <summary>
+ /// Creates a new object that is a copy of the current instance.
+ /// </summary>
+ /// <returns>
+ /// A new object that is a copy of this instance.
+ /// </returns>
+ ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() {
+ return new SigningBindingElementChain(this.signers.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray());
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs
new file mode 100644
index 0000000..46c2bd9
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------
+// <copyright file="TokenType.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ /// <summary>
+ /// The two types of tokens that exist in the OAuth protocol.
+ /// </summary>
+ public enum TokenType {
+ /// <summary>
+ /// A token that is freely issued to any known Consumer.
+ /// It does not grant any authorization to access protected resources,
+ /// but is used as a step in obtaining that access.
+ /// </summary>
+ RequestToken,
+
+ /// <summary>
+ /// A token only obtained after the owner of some protected resource(s)
+ /// has approved a Consumer's access to said resource(s).
+ /// </summary>
+ AccessToken,
+
+ /// <summary>
+ /// An unrecognized, expired or invalid token.
+ /// </summary>
+ InvalidToken,
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs
new file mode 100644
index 0000000..287ef01
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs
@@ -0,0 +1,77 @@
+//-----------------------------------------------------------------------
+// <copyright file="UriOrOobEncoding.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+
+ /// <summary>
+ /// An URI encoder that translates null <see cref="Uri"/> references as "oob"
+ /// instead of an empty/missing argument.
+ /// </summary>
+ internal class UriOrOobEncoding : IMessagePartNullEncoder {
+ /// <summary>
+ /// The string constant "oob", used to indicate an out-of-band configuration.
+ /// </summary>
+ private const string OutOfBandConfiguration = "oob";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriOrOobEncoding"/> class.
+ /// </summary>
+ public UriOrOobEncoding() {
+ }
+
+ #region IMessagePartNullEncoder Members
+
+ /// <summary>
+ /// Gets the string representation to include in a serialized message
+ /// when the message part has a <c>null</c> value.
+ /// </summary>
+ /// <value></value>
+ public string EncodedNullValue {
+ get { return OutOfBandConfiguration; }
+ }
+
+ #endregion
+
+ #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.AbsoluteUri;
+ }
+
+ /// <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) {
+ if (string.Equals(value, OutOfBandConfiguration, StringComparison.Ordinal)) {
+ return null;
+ } else {
+ return new Uri(value, UriKind.Absolute);
+ }
+ }
+
+ #endregion
+ }
+}