1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
|
//-----------------------------------------------------------------------
// <copyright file="OAuth1HttpMessageHandlerBase.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>
/// An implementation of 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;
/// <summary>
/// The default parameters location.
/// </summary>
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 or sets the length of the nonce.
/// </summary>
/// <value>
/// The length of the nonce.
/// </value>
public int NonceLength { get; set; }
/// <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>
/// 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.
/// </summary>
/// <param name="request">The request.</param>
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;
}
}
/// <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.ApplyAuthorization(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 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>
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;
}
/// <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>
/// <param name="dateTime">The date time.</param>
/// <returns>A string representation of the number of seconds since "the epoch".</returns>
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>
/// Collects and removes all query string parameters beginning with "oauth_" from the specified request,
/// and returns them as a collection.
/// </summary>
/// <param name="request">The request whose query string should be searched for "oauth_" parameters.</param>
/// <returns>The collection of parameters that were removed from the query string.</returns>
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;
}
/// <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>
/// <param name="oauthParameters">The oauth parameters.</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);
} 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();
}
}
}
|