//-----------------------------------------------------------------------
//
// 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();
}
}
}