diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
commit | 5fec515095ee10b522f414a03e78f282aaf520dc (patch) | |
tree | 204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.OAuth/OAuth/ChannelElements | |
parent | f1a4155398635a4fd9f485eec817152627682704 (diff) | |
parent | 8f4165ee515728aca3faaa26e8354a40612e85e4 (diff) | |
download | DotNetOpenAuth-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')
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&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 + } +} |