diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2013-02-18 22:08:07 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2013-02-18 22:08:07 -0800 |
commit | 32270c7413e7a2c37a02341a0894e2447f6d74f7 (patch) | |
tree | 05f6cf8566c7b4a2661a01d08967e91f05778837 | |
parent | 549017cdf590ea4ce4d8ad55c013c33a506133a3 (diff) | |
download | DotNetOpenAuth-32270c7413e7a2c37a02341a0894e2447f6d74f7.zip DotNetOpenAuth-32270c7413e7a2c37a02341a0894e2447f6d74f7.tar.gz DotNetOpenAuth-32270c7413e7a2c37a02341a0894e2447f6d74f7.tar.bz2 |
Matured the OAuth 1 consumer signing handler a bit.
10 files changed, 469 insertions, 97 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs index 48e6b4b..1ae9f84 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs @@ -135,8 +135,7 @@ namespace DotNetOpenAuth.ApplicationBlock { public static async Task<JArray> GetUpdatesAsync( ConsumerBase twitter, string accessToken, CancellationToken cancellationToken = default(CancellationToken)) { - var authorizingHandler = new OAuth1HttpMessageHandler(twitter.Channel.HostFactories.CreateHttpMessageHandler(), twitter, accessToken); - using (var httpClient = twitter.Channel.HostFactories.CreateHttpClient(authorizingHandler)) { + using (var httpClient = twitter.CreateHttpClient(accessToken)) { using (var response = await httpClient.GetAsync(GetFriendTimelineStatusEndpoint.Location, cancellationToken)) { response.EnsureSuccessStatusCode(); string jsonString = await response.Content.ReadAsStringAsync(); diff --git a/samples/OAuthConsumer/SignInWithTwitter.aspx.cs b/samples/OAuthConsumer/SignInWithTwitter.aspx.cs index b9d19ef..f90d557 100644 --- a/samples/OAuthConsumer/SignInWithTwitter.aspx.cs +++ b/samples/OAuthConsumer/SignInWithTwitter.aspx.cs @@ -9,8 +9,8 @@ using System.Web.UI.WebControls; using System.Xml.Linq; using System.Xml.XPath; - using DotNetOpenAuth.Messaging; using DotNetOpenAuth.ApplicationBlock; + using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth; public partial class SignInWithTwitter : System.Web.UI.Page { diff --git a/samples/OAuthConsumer/Twitter.aspx.cs b/samples/OAuthConsumer/Twitter.aspx.cs index a89c3bf..6ff6993 100644 --- a/samples/OAuthConsumer/Twitter.aspx.cs +++ b/samples/OAuthConsumer/Twitter.aspx.cs @@ -10,9 +10,8 @@ using System.Xml.Linq; using System.Xml.XPath; using DotNetOpenAuth.ApplicationBlock; - using DotNetOpenAuth.OAuth; - using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; public partial class Twitter : System.Web.UI.Page { private string AccessToken { diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index a4aff73..619f252 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -1611,6 +1611,19 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Enumerates all members of the collection as key=value pairs. + /// </summary> + internal static IEnumerable<KeyValuePair<string, string>> AsKeyValuePairs(this NameValueCollection nvc) { + Requires.NotNull(nvc, "nvc"); + + foreach (string key in nvc) { + foreach (string value in nvc.GetValues(key)) { + yield return new KeyValuePair<string, string>(key, value); + } + } + } + + /// <summary> /// Converts a <see cref="NameValueCollection"/> to an IDictionary<string, string>. /// </summary> /// <param name="nvc">The NameValueCollection to convert. May be null.</param> @@ -1881,6 +1894,11 @@ namespace DotNetOpenAuth.Messaging { internal static string EscapeUriDataStringRfc3986(string value) { Requires.NotNull(value, "value"); + // fast path for empty values. + if (value.Length == 0) { + return value; + } + // Start with RFC 2396 escaping by calling the .NET method to do the work. // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation). // If it does, the escaping we do that follows it will be a no-op since the diff --git a/src/DotNetOpenAuth.OAuth.Consumer/DotNetOpenAuth.OAuth.Consumer.csproj b/src/DotNetOpenAuth.OAuth.Consumer/DotNetOpenAuth.OAuth.Consumer.csproj index cda3b68..3d43a38 100644 --- a/src/DotNetOpenAuth.OAuth.Consumer/DotNetOpenAuth.OAuth.Consumer.csproj +++ b/src/DotNetOpenAuth.OAuth.Consumer/DotNetOpenAuth.OAuth.Consumer.csproj @@ -25,7 +25,8 @@ <Compile Include="OAuth\ChannelElements\RsaSha1ConsumerSigningBindingElement.cs" /> <Compile Include="OAuth\ConsumerBase.cs" /> <Compile Include="OAuth\DesktopConsumer.cs" /> - <Compile Include="OAuth\OAuth1HttpMessageHandler.cs" /> + <Compile Include="OAuth\OAuth1HmacSha1HttpMessageHandler.cs" /> + <Compile Include="OAuth\OAuth1HttpMessageHandlerBase.cs" /> <Compile Include="OAuth\WebConsumer.cs" /> <Compile Include="Properties\AssemblyInfo.cs"> <SubType> diff --git a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/ConsumerBase.cs index dcde81c..d72ad08 100644 --- a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/ConsumerBase.cs @@ -81,9 +81,33 @@ namespace DotNetOpenAuth.OAuth { /// <summary> /// Creates a message handler that signs outbound requests with a previously obtained authorization. /// </summary> - /// <returns>A message handler.</returns> - public OAuth1HttpMessageHandler CreateMessageHandler() { - return new OAuth1HttpMessageHandler(this); + /// <param name="accessToken">The access token to authorize outbound HTTP requests with.</param> + /// <param name="innerHandler">The inner handler that actually sends the HTTP message on the network.</param> + /// <returns> + /// A message handler. + /// </returns> + public OAuth1HttpMessageHandlerBase CreateMessageHandler(string accessToken = null, HttpMessageHandler innerHandler = null) { + return new OAuth1HmacSha1HttpMessageHandler() { + ConsumerKey = this.ConsumerKey, + ConsumerSecret = this.TokenManager.ConsumerSecret, + AccessToken = accessToken, + AccessTokenSecret = accessToken != null ? this.TokenManager.GetTokenSecret(accessToken) : null, + InnerHandler = innerHandler ?? this.Channel.HostFactories.CreateHttpMessageHandler(), + }; + } + + /// <summary> + /// Creates the HTTP client. + /// </summary> + /// <param name="accessToken">The access token to authorize outbound HTTP requests with.</param> + /// <param name="innerHandler">The inner handler that actually sends the HTTP message on the network.</param> + /// <returns>The HttpClient to use.</returns> + public HttpClient CreateHttpClient(string accessToken, HttpMessageHandler innerHandler = null) { + Requires.NotNullOrEmpty(accessToken, "accessToken"); + + var handler = this.CreateMessageHandler(accessToken, innerHandler); + var client = this.Channel.HostFactories.CreateHttpClient(handler); + return client; } /// <summary> diff --git a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HmacSha1HttpMessageHandler.cs b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HmacSha1HttpMessageHandler.cs new file mode 100644 index 0000000..11de257 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HmacSha1HttpMessageHandler.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth1HmacSha1HttpMessageHandler.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Security.Cryptography; + using System.Text; + using System.Threading.Tasks; + + /// <summary> + /// A delegating HTTP handler that signs outgoing HTTP requests + /// with an HMAC-SHA1 signature. + /// </summary> + public class OAuth1HmacSha1HttpMessageHandler : OAuth1HttpMessageHandlerBase { + /// <summary> + /// Initializes a new instance of the <see cref="OAuth1HmacSha1HttpMessageHandler"/> class. + /// </summary> + public OAuth1HmacSha1HttpMessageHandler() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth1HmacSha1HttpMessageHandler"/> class. + /// </summary> + /// <param name="innerHandler">The inner handler which is responsible for processing the HTTP response messages.</param> + public OAuth1HmacSha1HttpMessageHandler(HttpMessageHandler innerHandler) + : base(innerHandler) { + } + + /// <summary> + /// Calculates the signature for the specified buffer. + /// </summary> + /// <param name="signedPayload">The payload to calculate the signature for.</param> + /// <returns> + /// The signature. + /// </returns> + protected override byte[] Sign(byte[] signedPayload) { + using (var algorithm = HMACSHA1.Create()) { + algorithm.Key = Encoding.ASCII.GetBytes(this.GetConsumerAndTokenSecretString()); + return algorithm.ComputeHash(signedPayload); + } + } + + /// <summary> + /// Gets the signature method to include in the oauth_signature_method parameter. + /// </summary> + /// <value> + /// The signature method. + /// </value> + protected override string SignatureMethod { + get { return "HMAC-SHA1"; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandler.cs b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandler.cs deleted file mode 100644 index a763d5e..0000000 --- a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandler.cs +++ /dev/null @@ -1,83 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="OAuth1HttpMessageHandler.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OAuth { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - - using DotNetOpenAuth.Messaging; - - using Validation; - - /// <summary> - /// A delegated HTTP handler that automatically signs outgoing requests. - /// </summary> - public class OAuth1HttpMessageHandler : DelegatingHandler { - /// <summary> - /// Initializes a new instance of the <see cref="OAuth1HttpMessageHandler" /> class. - /// </summary> - /// <param name="consumer">The consumer.</param> - /// <param name="accessToken">The access token.</param> - public OAuth1HttpMessageHandler(ConsumerBase consumer = null, string accessToken = null) { - this.Consumer = consumer; - this.AccessToken = accessToken; - } - - /// <summary> - /// Initializes a new instance of the <see cref="OAuth1HttpMessageHandler" /> class. - /// </summary> - /// <param name="innerHandler">The inner handler.</param> - /// <param name="consumer">The consumer.</param> - /// <param name="accessToken">The access token.</param> - public OAuth1HttpMessageHandler(HttpMessageHandler innerHandler, ConsumerBase consumer = null, string accessToken = null) - : base(innerHandler) { - this.Consumer = consumer; - this.AccessToken = accessToken; - } - - /// <summary> - /// Gets or sets the access token. - /// </summary> - /// <value> - /// The access token. - /// </value> - public string AccessToken { get; set; } - - /// <summary> - /// Gets or sets the consumer. - /// </summary> - /// <value> - /// The consumer. - /// </value> - public ConsumerBase Consumer { get; set; } - - /// <summary> - /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. - /// </summary> - /// <param name="request">The HTTP request message to send to the server.</param> - /// <param name="cancellationToken">A cancellation token to cancel operation.</param> - /// <returns> - /// Returns <see cref="T:System.Threading.Tasks.Task`1" />. The task object representing the asynchronous operation. - /// </returns> - protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - Verify.Operation(this.Consumer != null, Strings.RequiredPropertyNotYetPreset, "Consumer"); - Verify.Operation(!string.IsNullOrEmpty(this.AccessToken), Strings.RequiredPropertyNotYetPreset, "AccessToken"); - - var deliveryMethods = MessagingUtilities.GetHttpDeliveryMethod(request.Method.Method) | HttpDeliveryMethods.AuthorizationHeaderRequest; - var signed = await - this.Consumer.PrepareAuthorizedRequestAsync( - new MessageReceivingEndpoint(request.RequestUri, deliveryMethods), this.AccessToken, cancellationToken); - request.Headers.Authorization = signed.Headers.Authorization; - - return await base.SendAsync(request, cancellationToken); - } - } -} diff --git a/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandlerBase.cs b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandlerBase.cs new file mode 100644 index 0000000..6029d47 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandlerBase.cs @@ -0,0 +1,355 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth1HttpMessageHandler.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using System.Web; + using DotNetOpenAuth.Messaging; + using Validation; + + /// <summary> + /// A base class for delegating <see cref="HttpMessageHandler" />s that sign + /// outgoing HTTP requests per the OAuth 1.0 "3.4 Signature" in RFC 5849. + /// </summary> + /// <remarks> + /// http://tools.ietf.org/html/rfc5849#section-3.4 + /// </remarks> + public abstract class OAuth1HttpMessageHandlerBase : DelegatingHandler { + /// <summary> + /// These are the characters that may be chosen from when forming a random nonce. + /// </summary> + private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /// <summary> + /// The default nonce length. + /// </summary> + private const int defaultNonceLength = 8; + + private const OAuthParametersLocation defaultParametersLocation = OAuthParametersLocation.AuthorizationHttpHeader; + + /// <summary> + /// The reference date and time for calculating time stamps. + /// </summary> + private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// <summary> + /// An array containing simply the amperstand character. + /// </summary> + private static readonly char[] ParameterSeparatorAsArray = new char[] { '&' }; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth1HttpMessageHandlerBase"/> class. + /// </summary> + protected OAuth1HttpMessageHandlerBase() { + this.NonceLength = defaultNonceLength; + this.Location = defaultParametersLocation; + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth1HttpMessageHandlerBase"/> class. + /// </summary> + /// <param name="innerHandler">The inner handler which is responsible for processing the HTTP response messages.</param> + protected OAuth1HttpMessageHandlerBase(HttpMessageHandler innerHandler) + : base(innerHandler) { + this.NonceLength = defaultNonceLength; + this.Location = defaultParametersLocation; + } + + /// <summary> + /// The locations that oauth parameters may be added to HTTP requests. + /// </summary> + public enum OAuthParametersLocation { + /// <summary> + /// The oauth parameters are added to the query string in the URL. + /// </summary> + QueryString, + + /// <summary> + /// An HTTP Authorization header is added with the OAuth scheme. + /// </summary> + AuthorizationHttpHeader, + } + + /// <summary> + /// Gets or sets the location to add OAuth parameters to outbound HTTP requests. + /// </summary> + public OAuthParametersLocation Location { get; set; } + + /// <summary> + /// Gets or sets the consumer key. + /// </summary> + /// <value> + /// The consumer key. + /// </value> + public string ConsumerKey { get; set; } + + /// <summary> + /// Gets or sets the consumer secret. + /// </summary> + /// <value> + /// The consumer secret. + /// </value> + public string ConsumerSecret { get; set; } + + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value> + /// The access token. + /// </value> + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets the access token secret. + /// </summary> + /// <value> + /// The access token secret. + /// </value> + public string AccessTokenSecret { get; set; } + + /// <summary> + /// Gets the length of the nonce. + /// </summary> + /// <value> + /// The length of the nonce. + /// </value> + public int NonceLength { get; set; } + + /// <summary> + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// </summary> + /// <param name="request">The HTTP request message to send to the server.</param> + /// <param name="cancellationToken">A cancellation token to cancel operation.</param> + /// <returns> + /// Returns <see cref="T:System.Threading.Tasks.Task`1" />. The task object representing the asynchronous operation. + /// </returns> + protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + Requires.NotNull(request, "request"); + cancellationToken.ThrowIfCancellationRequested(); + this.ApplyOAuthParameters(request); + return base.SendAsync(request, cancellationToken); + } + + /// <summary> + /// Calculates the signature for the specified buffer. + /// </summary> + /// <param name="signedPayload">The payload to calculate the signature for.</param> + /// <returns>The signature.</returns> + protected abstract byte[] Sign(byte[] signedPayload); + + /// <summary> + /// Gets the signature method to include in the oauth_signature_method parameter. + /// </summary> + /// <value> + /// The signature method. + /// </value> + protected abstract string SignatureMethod { get; } + + /// <summary> + /// Gets the "ConsumerSecret&AccessTokenSecret" string, allowing either property to be empty or null. + /// </summary> + /// <returns>The concatenated string.</returns> + /// <remarks> + /// This is useful in the PLAINTEXT and HMAC-SHA1 signature algorithms. + /// </remarks> + protected string GetConsumerAndTokenSecretString() { + var builder = new StringBuilder(); + builder.Append(UrlEscape(this.ConsumerSecret ?? string.Empty)); + builder.Append("&"); + builder.Append(UrlEscape(this.AccessTokenSecret ?? string.Empty)); + return builder.ToString(); + } + + /// <summary> + /// Escapes a value for transport in a URI, per RFC 3986. + /// </summary> + /// <param name="value">The value to escape. Null and empty strings are OK.</param> + /// <returns>The escaped value. Never null.</returns> + private static string UrlEscape(string value) { + return MessagingUtilities.EscapeUriDataStringRfc3986(value ?? string.Empty); + } + + /// <summary> + /// Returns the OAuth 1.0 timestamp for the current time. + /// </summary> + private static string ToTimeStamp(DateTime dateTime) { + Requires.Argument(dateTime.Kind == DateTimeKind.Utc, "dateTime", "UTC time required"); + TimeSpan ts = dateTime - epoch; + long secondsSinceEpoch = (long)ts.TotalSeconds; + return secondsSinceEpoch.ToString(CultureInfo.InvariantCulture); + } + + /// <summary> + /// Constructs the "Base String URI" as described in http://tools.ietf.org/html/rfc5849#section-3.4.1.2 + /// </summary> + /// <param name="requestUri">The request URI.</param> + /// <returns>The string to include in the signature base string.</returns> + private static string GetBaseStringUri(Uri requestUri) { + Requires.NotNull(requestUri, "requestUri"); + + var endpoint = new UriBuilder(requestUri); + endpoint.Query = null; + endpoint.Fragment = null; + return endpoint.Uri.AbsoluteUri; + } + + /// <summary> + /// Constructs the "Signature Base String" as described in http://tools.ietf.org/html/rfc5849#section-3.4.1 + /// </summary> + /// <param name="request">The HTTP request message.</param> + /// <returns>The signature base string.</returns> + private string ConstructSignatureBaseString(HttpRequestMessage request, NameValueCollection oauthParameters) { + Requires.NotNull(request, "request"); + Requires.NotNull(oauthParameters, "oauthParameters"); + + var builder = new StringBuilder(); + builder.Append(UrlEscape(request.Method.ToString().ToUpperInvariant())); + builder.Append("&"); + builder.Append(UrlEscape(GetBaseStringUri(request.RequestUri))); + builder.Append("&"); + builder.Append(UrlEscape(this.GetNormalizedParameters(request, oauthParameters))); + + return builder.ToString(); + } + + /// <summary> + /// Generates a string of random characters for use as a nonce. + /// </summary> + /// <returns>The nonce string.</returns> + private string GenerateUniqueFragment() { + return MessagingUtilities.GetRandomString(this.NonceLength, AllowedCharacters); + } + + /// <summary> + /// Gets the "oauth_" prefixed parameters that should be added to an outbound request. + /// </summary> + /// <returns>A collection of name=value pairs.</returns> + private NameValueCollection GetOAuthParameters() { + var nvc = new NameValueCollection(8); + nvc.Add("oauth_version", "1.0"); + nvc.Add("oauth_nonce", this.GenerateUniqueFragment()); + nvc.Add("oauth_timestamp", ToTimeStamp(DateTime.UtcNow)); + nvc.Add("oauth_signature_method", this.SignatureMethod); + nvc.Add("oauth_consumer_key", this.ConsumerKey); + if (!string.IsNullOrEmpty(this.AccessToken)) { + nvc.Add("oauth_token", this.AccessToken); + } + + return nvc; + } + + /// <summary> + /// Gets a normalized string of the query string parameters included in the request and the additional OAuth parameters. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <param name="oauthParameters">The oauth parameters that will be added to the request.</param> + /// <returns>The normalized string of parameters to included in the signature base string.</returns> + private string GetNormalizedParameters(HttpRequestMessage request, NameValueCollection oauthParameters) { + Requires.NotNull(request, "request"); + Requires.NotNull(oauthParameters, "oauthParameters"); + + NameValueCollection nvc; + if (request.RequestUri.Query != null) { + // NameValueCollection does support non-unique keys, as long as you use it carefully. + nvc = HttpUtility.ParseQueryString(request.RequestUri.Query); + + // Remove any parameters beginning with "oauth_" + var keysToRemove = nvc.Cast<string>().Where(k => k.StartsWith(Protocol.ParameterPrefix, StringComparison.Ordinal)).ToList(); + foreach (string key in keysToRemove) { + nvc.Remove(key); + } + } else { + nvc = new NameValueCollection(8); + } + + // Add OAuth parameters. + nvc.Add(oauthParameters); + + // Now convert the NameValueCollection into an ordered list, and properly escape all keys and value while we're at it. + var list = new List<KeyValuePair<string, string>>(nvc.Count); + foreach (var pair in nvc.AsKeyValuePairs()) { + string escapedKey = UrlEscape(pair.Key); + string escapedValue = UrlEscape(pair.Value ?? string.Empty); // value can be null if no "=" appears in the query string for this key. + list.Add(new KeyValuePair<string, string>(escapedKey, escapedValue)); + } + + // Sort the parameters + list.Sort((kv1, kv2) => { + int compare = string.Compare(kv1.Key, kv2.Key, StringComparison.Ordinal); + if (compare != 0) { + return compare; + } + + return string.Compare(kv1.Value, kv2.Value, StringComparison.Ordinal); + }); + + // Convert this sorted list into a single concatenated string. + var normalizedParameterString = new StringBuilder(); + foreach (var pair in list) { + if (normalizedParameterString.Length > 0) { + normalizedParameterString.Append("&"); + } + + normalizedParameterString.Append(pair.Key); + normalizedParameterString.Append("="); + normalizedParameterString.Append(pair.Value); + } + + return normalizedParameterString.ToString(); + } + + /// <summary> + /// Applies OAuth authorization to the specified request. + /// </summary> + private void ApplyOAuthParameters(HttpRequestMessage request) { + Requires.NotNull(request, "request"); + + var oauthParameters = this.GetOAuthParameters(); + string signature = this.GetSignature(request, oauthParameters); + oauthParameters.Add("oauth_signature", signature); + + // Add parameters and signature to request. + switch (this.Location) { + case OAuthParametersLocation.AuthorizationHttpHeader: + request.Headers.Authorization = new AuthenticationHeaderValue(Protocol.AuthorizationHeaderScheme, MessagingUtilities.AssembleAuthorizationHeader(oauthParameters.AsKeyValuePairs())); + break; + case OAuthParametersLocation.QueryString: + var uriBuilder = new UriBuilder(request.RequestUri); + uriBuilder.AppendQueryArgs(oauthParameters.AsKeyValuePairs()); + request.RequestUri = uriBuilder.Uri; + break; + } + } + + /// <summary> + /// Gets the OAuth 1.0 signature to apply to the specified request. + /// </summary> + /// <param name="request">The outbound HTTP request.</param> + /// <param name="oauthParameters">The oauth parameters.</param> + /// <returns> + /// The value for the "oauth_signature" parameter. + /// </returns> + private string GetSignature(HttpRequestMessage request, NameValueCollection oauthParameters) { + Requires.NotNull(request, "request"); + Requires.NotNull(oauthParameters, "oauthParameters"); + + string signatureBaseString = this.ConstructSignatureBaseString(request, oauthParameters); + byte[] signatureBaseStringBytes = Encoding.ASCII.GetBytes(signatureBaseString); + byte[] signatureBytes = this.Sign(signatureBaseStringBytes); + string signatureString = Convert.ToBase64String(signatureBytes); + return signatureString; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth.OpenId/OpenId/UntrustedWebRequestHandler.cs index b76d6ad..f1b7e81 100644 --- a/src/DotNetOpenAuth.OpenId/OpenId/UntrustedWebRequestHandler.cs +++ b/src/DotNetOpenAuth.OpenId/OpenId/UntrustedWebRequestHandler.cs @@ -38,6 +38,11 @@ namespace DotNetOpenAuth.OpenId { /// </remarks> public class UntrustedWebRequestHandler : HttpMessageHandler { /// <summary> + /// The inner handler. + /// </summary> + private readonly InternalWebRequestHandler innerHandler; + + /// <summary> /// The set of URI schemes allowed in untrusted web requests. /// </summary> private ICollection<string> allowableSchemes = new List<string> { "http", "https" }; @@ -69,11 +74,6 @@ namespace DotNetOpenAuth.OpenId { private int maximumRedirections = Configuration.MaximumRedirections; /// <summary> - /// The inner handler. - /// </summary> - private readonly InternalWebRequestHandler innerHandler; - - /// <summary> /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler" /> class. /// </summary> /// <param name="innerHandler"> |