//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- 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; /// /// A base class for delegating s that sign /// outgoing HTTP requests per the OAuth 1.0 "3.4 Signature" in RFC 5849. /// /// /// An implementation of http://tools.ietf.org/html/rfc5849#section-3.4 /// public abstract class OAuth1HttpMessageHandlerBase : DelegatingHandler { /// /// These are the characters that may be chosen from when forming a random nonce. /// private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; /// /// The default nonce length. /// private const int DefaultNonceLength = 8; /// /// The default parameters location. /// private const OAuthParametersLocation DefaultParametersLocation = OAuthParametersLocation.AuthorizationHttpHeader; /// /// The reference date and time for calculating time stamps. /// private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// /// An array containing simply the amperstand character. /// private static readonly char[] ParameterSeparatorAsArray = new char[] { '&' }; /// /// Initializes a new instance of the class. /// protected OAuth1HttpMessageHandlerBase() { this.NonceLength = DefaultNonceLength; this.Location = DefaultParametersLocation; } /// /// Initializes a new instance of the class. /// /// The inner handler which is responsible for processing the HTTP response messages. protected OAuth1HttpMessageHandlerBase(HttpMessageHandler innerHandler) : base(innerHandler) { this.NonceLength = DefaultNonceLength; this.Location = DefaultParametersLocation; } /// /// The locations that oauth parameters may be added to HTTP requests. /// public enum OAuthParametersLocation { /// /// The oauth parameters are added to the query string in the URL. /// QueryString, /// /// An HTTP Authorization header is added with the OAuth scheme. /// AuthorizationHttpHeader, } /// /// Gets or sets the location to add OAuth parameters to outbound HTTP requests. /// public OAuthParametersLocation Location { get; set; } /// /// Gets or sets the consumer key. /// /// /// The consumer key. /// public string ConsumerKey { get; set; } /// /// Gets or sets the consumer secret. /// /// /// The consumer secret. /// public string ConsumerSecret { get; set; } /// /// Gets or sets the access token. /// /// /// The access token. /// public string AccessToken { get; set; } /// /// Gets or sets the access token secret. /// /// /// The access token secret. /// public string AccessTokenSecret { get; set; } /// /// Gets or sets the length of the nonce. /// /// /// The length of the nonce. /// public int NonceLength { get; set; } /// /// Gets the signature method to include in the oauth_signature_method parameter. /// /// /// The signature method. /// protected abstract string SignatureMethod { get; } /// /// Applies OAuth authorization to the specified request. /// This method is applied automatically to outbound requests that use this message handler instance. /// However this method may be useful for obtaining the OAuth 1.0 signature without actually sending the request. /// /// The request. public void ApplyAuthorization(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: // Some oauth parameters may have been put in the query string of the original message. // We want to move any that we find into the authorization header. oauthParameters.Add(ExtractOAuthParametersFromQueryString(request)); 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; } } /// /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. /// /// The HTTP request message to send to the server. /// A cancellation token to cancel operation. /// /// Returns . The task object representing the asynchronous operation. /// protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); cancellationToken.ThrowIfCancellationRequested(); this.ApplyAuthorization(request); return base.SendAsync(request, cancellationToken); } /// /// Calculates the signature for the specified buffer. /// /// The payload to calculate the signature for. /// The signature. protected abstract byte[] Sign(byte[] signedPayload); /// /// Gets the OAuth 1.0 signature to apply to the specified request. /// /// The outbound HTTP request. /// The oauth parameters. /// /// The value for the "oauth_signature" parameter. /// protected virtual 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; } /// /// Gets the "ConsumerSecret&AccessTokenSecret" string, allowing either property to be empty or null. /// /// The concatenated string. /// /// This is useful in the PLAINTEXT and HMAC-SHA1 signature algorithms. /// 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(); } /// /// Escapes a value for transport in a URI, per RFC 3986. /// /// The value to escape. Null and empty strings are OK. /// The escaped value. Never null. private static string UrlEscape(string value) { return MessagingUtilities.EscapeUriDataStringRfc3986(value ?? string.Empty); } /// /// Returns the OAuth 1.0 timestamp for the current time. /// /// The date time. /// A string representation of the number of seconds since "the epoch". 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); } /// /// Constructs the "Base String URI" as described in http://tools.ietf.org/html/rfc5849#section-3.4.1.2 /// /// The request URI. /// /// The string to include in the signature base string. /// 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; } /// /// Collects and removes all query string parameters beginning with "oauth_" from the specified request, /// and returns them as a collection. /// /// The request whose query string should be searched for "oauth_" parameters. /// The collection of parameters that were removed from the query string. private static NameValueCollection ExtractOAuthParametersFromQueryString(HttpRequestMessage request) { Requires.NotNull(request, "request"); var extracted = new NameValueCollection(); if (!string.IsNullOrEmpty(request.RequestUri.Query)) { var queryString = HttpUtility.ParseQueryString(request.RequestUri.Query); foreach (var pair in queryString.AsKeyValuePairs()) { if (pair.Key.StartsWith(Protocol.ParameterPrefix, StringComparison.Ordinal)) { extracted.Add(pair.Key, pair.Value); } } if (extracted.Count > 0) { foreach (string key in extracted) { queryString.Remove(key); } var modifiedRequestUri = new UriBuilder(request.RequestUri); modifiedRequestUri.Query = MessagingUtilities.CreateQueryString(queryString.AsKeyValuePairs()); request.RequestUri = modifiedRequestUri.Uri; } } return extracted; } /// /// Constructs the "Signature Base String" as described in http://tools.ietf.org/html/rfc5849#section-3.4.1 /// /// The HTTP request message. /// The oauth parameters. /// /// The signature base string. /// 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(); } /// /// Generates a string of random characters for use as a nonce. /// /// The nonce string. private string GenerateUniqueFragment() { return MessagingUtilities.GetRandomString(this.NonceLength, AllowedCharacters); } /// /// Gets the "oauth_" prefixed parameters that should be added to an outbound request. /// /// A collection of name=value pairs. 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; } /// /// Gets a normalized string of the query string parameters included in the request and the additional OAuth parameters. /// /// The HTTP request. /// The oauth parameters that will be added to the request. /// The normalized string of parameters to included in the signature base string. 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); } 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>(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(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(); } } }