Statistics
| Branch: | Revision:

root / trunk / hammock / src / net35 / Hammock / Authentication / OAuth / OAuthTools.cs @ 0eea575a

History | View | Annotate | Download (13 kB)

1
using System;
2
using System.Linq;
3
using System.Security.Cryptography;
4
using System.Text;
5
using Hammock.Extensions;
6
using Hammock.Web;
7

    
8
#if NETCF
9
using Hammock.Security.Cryptography;
10
#endif
11

    
12
namespace Hammock.Authentication.OAuth
13
{
14
#if !SILVERLIGHT
15
    [Serializable]
16
#endif
17
    public static class OAuthTools
18
    {
19
        private const string AlphaNumeric = Upper + Lower + Digit;
20
        private const string Digit = "1234567890";
21
        private const string Lower = "abcdefghijklmnopqrstuvwxyz";
22
        private const string Unreserved = AlphaNumeric + "-._~";
23
        private const string Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
24

    
25
        private static readonly Random _random;
26
        private static readonly object _randomLock = new object();
27

    
28
#if !SILVERLIGHT
29
        private static readonly RandomNumberGenerator _rng =
30
            RandomNumberGenerator.Create();
31
#endif
32

    
33
        static OAuthTools()
34
        {
35
#if !SILVERLIGHT
36
            var bytes = new byte[4];
37
            _rng.GetNonZeroBytes(bytes);
38
            _random = new Random(BitConverter.ToInt32(bytes, 0));
39
#else
40
            _random = new Random();
41
#endif
42
        }
43

    
44
        /// <summary>
45
        /// All text parameters are UTF-8 encoded (per section 5.1).
46
        /// </summary>
47
        /// <seealso cref="http://www.hueniverse.com/hueniverse/2008/10/beginners-gui-1.html"/> 
48
        private static readonly Encoding _encoding = Encoding.UTF8;
49

    
50
        /// <summary>
51
        /// Generates a random 16-byte lowercase alphanumeric string. 
52
        /// </summary>
53
        /// <seealso cref="http://oauth.net/core/1.0#nonce"/>
54
        /// <returns></returns>
55
        public static string GetNonce()
56
        {
57
            const string chars = (Lower + Digit);
58

    
59
            var nonce = new char[16];
60
            lock (_randomLock)
61
            {
62
                for (var i = 0; i < nonce.Length; i++)
63
                {
64
                    nonce[i] = chars[_random.Next(0, chars.Length)];
65
                }
66
            }
67
            return new string(nonce);
68
        }
69

    
70
        /// <summary>
71
        /// Generates a timestamp based on the current elapsed seconds since '01/01/1970 0000 GMT"
72
        /// </summary>
73
        /// <seealso cref="http://oauth.net/core/1.0#nonce"/>
74
        /// <returns></returns>
75
        public static string GetTimestamp()
76
        {
77
            return GetTimestamp(DateTime.UtcNow);
78
        }
79

    
80
        /// <summary>
81
        /// Generates a timestamp based on the elapsed seconds of a given time since '01/01/1970 0000 GMT"
82
        /// </summary>
83
        /// <seealso cref="http://oauth.net/core/1.0#nonce"/>
84
        /// <param name="dateTime">A specified point in time.</param>
85
        /// <returns></returns>
86
        public static string GetTimestamp(DateTime dateTime)
87
        {
88
            var timestamp = dateTime.ToUnixTime();
89
            return timestamp.ToString();
90
        }
91

    
92
        /// <summary>
93
        /// URL encodes a string based on section 5.1 of the OAuth spec.
94
        /// Namely, percent encoding with [RFC3986], avoiding unreserved characters,
95
        /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs.
96
        /// </summary>
97
        /// <param name="value"></param>
98
        /// <seealso cref="http://oauth.net/core/1.0#encoding_parameters" />
99
        public static string UrlEncodeRelaxed(string value)
100
        {
101
            var escaped = Uri.EscapeDataString(value);
102

    
103
            // LinkedIn users have problems because it requires escaping brackets
104
            escaped = escaped.Replace("(", "(".PercentEncode())
105
                             .Replace(")", ")".PercentEncode());
106

    
107
            return escaped;
108
        }
109

    
110
        /// <summary>
111
        /// URL encodes a string based on section 5.1 of the OAuth spec.
112
        /// Namely, percent encoding with [RFC3986], avoiding unreserved characters,
113
        /// upper-casing hexadecimal characters, and UTF-8 encoding for text value pairs.
114
        /// </summary>
115
        /// <param name="value"></param>
116
        /// <seealso cref="http://oauth.net/core/1.0#encoding_parameters" />
117
        public static string UrlEncodeStrict(string value)
118
        {
119
            // [JD]: We need to escape the apostrophe as well or the signature will fail
120
            var original = value;
121
            var ret = original.Where(
122
                c => !Unreserved.Contains(c) && c != '%').Aggregate(
123
                    value, (current, c) => current.Replace(
124
                          c.ToString(), c.ToString().PercentEncode()
125
                          ));
126

    
127
            return ret.Replace("%%", "%25%"); // Revisit to encode actual %'s
128
        }
129

    
130
        /// <summary>
131
        /// Sorts a collection of key-value pairs by name, and then value if equal,
132
        /// concatenating them into a single string. This string should be encoded
133
        /// prior to, or after normalization is run.
134
        /// </summary>
135
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.1.1"/>
136
        /// <param name="parameters"></param>
137
        /// <returns></returns>
138
        public static string NormalizeRequestParameters(WebParameterCollection parameters)
139
        {
140
            var copy = SortParametersExcludingSignature(parameters);
141
            var concatenated = copy.Concatenate("=", "&");
142
            return concatenated;
143
        }
144

    
145
        /// <summary>
146
        /// Sorts a <see cref="WebParameterCollection"/> by name, and then value if equal.
147
        /// </summary>
148
        /// <param name="parameters">A collection of parameters to sort</param>
149
        /// <returns>A sorted parameter collection</returns>
150
        public static WebParameterCollection SortParametersExcludingSignature(WebParameterCollection parameters)
151
        {
152
            var copy = new WebParameterCollection(parameters);
153
            var exclusions = copy.Where(n => n.Name.EqualsIgnoreCase("oauth_signature"));
154

    
155
            copy.RemoveAll(exclusions);
156
            copy.ForEach(p => p.Value = UrlEncodeStrict(p.Value));
157
            copy.Sort((x, y) => x.Name.Equals(y.Name) ? x.Value.CompareTo(y.Value) : x.Name.CompareTo(y.Name));
158
            return copy;
159
        }
160

    
161
        /// <summary>
162
        /// Creates a request URL suitable for making OAuth requests.
163
        /// Resulting URLs must exclude port 80 or port 443 when accompanied by HTTP and HTTPS, respectively.
164
        /// Resulting URLs must be lower case.
165
        /// </summary>
166
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.1.2"/>
167
        /// <param name="url">The original request URL</param>
168
        /// <returns></returns>
169
        public static string ConstructRequestUrl(Uri url)
170
        {
171
            if (url == null)
172
            {
173
                throw new ArgumentNullException("url");
174
            }
175

    
176
            var sb = new StringBuilder();
177

    
178
            var requestUrl = "{0}://{1}".FormatWith(url.Scheme, url.Host);
179
            var qualified = ":{0}".FormatWith(url.Port);
180
            var basic = url.Scheme == "http" && url.Port == 80;
181
            var secure = url.Scheme == "https" && url.Port == 443;
182

    
183
            sb.Append(requestUrl);
184
            sb.Append(!basic && !secure ? qualified : "");
185
            sb.Append(url.AbsolutePath);
186

    
187
            return sb.ToString(); //.ToLower();
188
        }
189

    
190
        /// <summary>
191
        /// Creates a request elements concatentation value to send with a request. 
192
        /// This is also known as the signature base.
193
        /// </summary>
194
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.1.3"/>
195
        /// <seealso cref="http://oauth.net/core/1.0#sig_base_example"/>
196
        /// <param name="method">The request's HTTP method type</param>
197
        /// <param name="url">The request URL</param>
198
        /// <param name="parameters">The request's parameters</param>
199
        /// <returns>A signature base string</returns>
200
        public static string ConcatenateRequestElements(WebMethod method, string url, WebParameterCollection parameters)
201
        {
202
            var sb = new StringBuilder();
203

    
204
            // Separating &'s are not URL encoded
205
            var requestMethod = method.ToUpper().Then("&");
206
            var requestUrl = UrlEncodeRelaxed(ConstructRequestUrl(url.AsUri())).Then("&");
207
            var requestParameters = UrlEncodeRelaxed(NormalizeRequestParameters(parameters));
208
            
209
            sb.Append(requestMethod);
210
            sb.Append(requestUrl);
211
            sb.Append(requestParameters);
212

    
213
            return sb.ToString();
214
        }
215

    
216
        /// <summary>
217
        /// Creates a signature value given a signature base and the consumer secret.
218
        /// This method is used when the token secret is currently unknown.
219
        /// </summary>
220
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.2"/>
221
        /// <param name="signatureMethod">The hashing method</param>
222
        /// <param name="signatureBase">The signature base</param>
223
        /// <param name="consumerSecret">The consumer key</param>
224
        /// <returns></returns>
225
        public static string GetSignature(OAuthSignatureMethod signatureMethod, 
226
                                          string signatureBase,
227
                                          string consumerSecret)
228
        {
229
            return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null);
230
        }
231

    
232
        /// <summary>
233
        /// Creates a signature value given a signature base and the consumer secret.
234
        /// This method is used when the token secret is currently unknown.
235
        /// </summary>
236
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.2"/>
237
        /// <param name="signatureMethod">The hashing method</param>
238
        /// <param name="signatureTreatment">The treatment to use on a signature value</param>
239
        /// <param name="signatureBase">The signature base</param>
240
        /// <param name="consumerSecret">The consumer key</param>
241
        /// <returns></returns>
242
        public static string GetSignature(OAuthSignatureMethod signatureMethod,
243
                                          OAuthSignatureTreatment signatureTreatment, 
244
                                          string signatureBase,
245
                                          string consumerSecret)
246
        {
247
            return GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null);
248
        }
249

    
250
        /// <summary>
251
        /// Creates a signature value given a signature base and the consumer secret and a known token secret.
252
        /// </summary>
253
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.2"/>
254
        /// <param name="signatureMethod">The hashing method</param>
255
        /// <param name="signatureBase">The signature base</param>
256
        /// <param name="consumerSecret">The consumer secret</param>
257
        /// <param name="tokenSecret">The token secret</param>
258
        /// <returns></returns>
259
        public static string GetSignature(OAuthSignatureMethod signatureMethod, 
260
                                          string signatureBase,
261
                                          string consumerSecret,
262
                                          string tokenSecret)
263
        {
264
            return GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, consumerSecret, tokenSecret);
265
        }
266

    
267
        /// <summary>
268
        /// Creates a signature value given a signature base and the consumer secret and a known token secret.
269
        /// </summary>
270
        /// <seealso cref="http://oauth.net/core/1.0#rfc.section.9.2"/>
271
        /// <param name="signatureMethod">The hashing method</param>
272
        /// <param name="signatureTreatment">The treatment to use on a signature value</param>
273
        /// <param name="signatureBase">The signature base</param>
274
        /// <param name="consumerSecret">The consumer secret</param>
275
        /// <param name="tokenSecret">The token secret</param>
276
        /// <returns></returns>
277
        public static string GetSignature(OAuthSignatureMethod signatureMethod, 
278
                                          OAuthSignatureTreatment signatureTreatment,
279
                                          string signatureBase,
280
                                          string consumerSecret,
281
                                          string tokenSecret)
282
        {
283
            if (tokenSecret.IsNullOrBlank())
284
            {
285
                tokenSecret = String.Empty;
286
            }
287

    
288
            consumerSecret = UrlEncodeRelaxed(consumerSecret);
289
            tokenSecret = UrlEncodeRelaxed(tokenSecret);
290

    
291
            string signature;
292
            switch (signatureMethod)
293
            {
294
                case OAuthSignatureMethod.HmacSha1:
295
                    {
296
                        var crypto = new HMACSHA1();
297
                        var key = "{0}&{1}".FormatWith(consumerSecret, tokenSecret);
298

    
299
                        crypto.Key = _encoding.GetBytes(key);
300
                        signature = signatureBase.HashWith(crypto);
301

    
302
                        break;
303
                    }
304
                default:
305
                    throw new NotImplementedException("Only HMAC-SHA1 is currently supported.");
306
            }
307

    
308
            var result = signatureTreatment == OAuthSignatureTreatment.Escaped
309
                       ? UrlEncodeRelaxed(signature)
310
                       : signature;
311

    
312
            return result;
313
        }
314
    }
315
}