OAuth Helper in C#

9/18/2009

The app that I've been working on in my spare time (well one of them anyway), interfaces with netflix. Netflix, for those who are unaware, has a developer API that you can use in various applications. The main issue that you'll run into is that the documentation that they give is, well, it could be better. Especially if you've never used OAuth before as it is used along with REST in their API.

OAuth is a protocol that is used to help in authorization. When it comes down to it, it's just an URL with the information added as a query string. This makes it extremely easy to use, especially if you have some experience with it. I am unfortunately one of those individuals that had never used it before, but thankfully the specs are pretty straightforward enough. Anyway, after taking a look at some other people's code, I came up with a base class to help me if I ever need to do anything with OAuth again:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Collections.Generic;
  25: using System.Security.Cryptography;
  26: using System.Text;
  27: using Utilities.DataTypes;
  28: #endregion
  29:  
  30: namespace Utilities.Web.OAuth
  31: {
  32:     /// <summary>
  33:     /// OAuth base class
  34:     /// </summary>
  35:     public class OAuth
  36:     {
  37:         #region Constructor
  38:  
  39:         /// <summary>
  40:         /// Constructor
  41:         /// </summary>
  42:         public OAuth()
  43:         {
  44:             Parameters = new System.Collections.Generic.List<Pair<string, string>>();
  45:             AddParameter("oauth_consumer_key", "");
  46:             AddParameter("oauth_nonce", "");
  47:             AddParameter("oauth_signature_method", "");
  48:             AddParameter("oauth_timestamp", "");
  49:             AddParameter("oauth_version", "1.0");
  50:             RandomGenerator = new Random.Random();
  51:         }
  52:  
  53:         #endregion
  54:  
  55:         #region Protected Functions
  56:  
  57:         /// <summary>
  58:         /// Generates a request
  59:         /// </summary>
  60:         /// <returns>The string containing the request</returns>
  61:         protected string GenerateRequest()
  62:         {
  63:             string Url = "";
  64:             string Parameters = "";
  65:             string Signature = GenerateSignature(out Url, out Parameters);
  66:             string ReturnUrl = Url.ToString() + "?" + Parameters + "&oauth_signature=" + UrlEncode(Signature);
  67:             return ReturnUrl;
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Generates the signature
  72:         /// </summary>
  73:         /// <param name="Url">Url</param>
  74:         /// <param name="Parameters">Parameters</param>
  75:         /// <returns>The signature</returns>
  76:         protected string GenerateSignature(out string Url, out string Parameters)
  77:         {
  78:             Parameters = "";
  79:             Url = "";
  80:  
  81:             if (this.SignatureType == Signature.HMACSHA1)
  82:             {
  83:                 string Base = GenerateBase(out Url, out Parameters);
  84:                 HMACSHA1 SHA1 = new HMACSHA1();
  85:                 SHA1.Key = Encoding.ASCII.GetBytes(UrlEncode(ConsumerKeySecret) + "&" + (string.IsNullOrEmpty(TokenSecret) ? "" : UrlEncode(TokenSecret)));
  86:                 return Convert.ToBase64String(SHA1.ComputeHash(System.Text.Encoding.ASCII.GetBytes(Base)));
  87:             }
  88:             else if (this.SignatureType == Signature.RSASHA1)
  89:             {
  90:                 throw new NotImplementedException();
  91:             }
  92:             else if (this.SignatureType == Signature.PLAINTEXT)
  93:             {
  94:                 return UrlEncode(ConsumerKeySecret + "&" + TokenSecret);
  95:             }
  96:             return "";
  97:         }
  98:  
  99:         /// <summary>
 100:         /// Does url encoding using uppercase since that is needed for .Net
 101:         /// </summary>
 102:         /// <param name="Input">Input string</param>
 103:         /// <returns>Url encoded string</returns>
 104:         protected string UrlEncode(string Input)
 105:         {
 106:             StringBuilder Result = new StringBuilder();
 107:             for (int x = 0; x < Input.Length; ++x)
 108:             {
 109:                 if (UnreservedChars.IndexOf(Input[x]) != -1)
 110:                     Result.Append(Input[x]);
 111:                 else
 112:                     Result.Append("%").Append(String.Format("{0:X2}", (int)Input[x]));
 113:             }
 114:             return Result.ToString();
 115:         }
 116:  
 117:         /// <summary>
 118:         /// Adds a parameter
 119:         /// </summary>
 120:         /// <param name="Key">Key text</param>
 121:         /// <param name="Value">Value text</param>
 122:         protected void AddParameter(string Key, string Value)
 123:         {
 124:             bool Found = false;
 125:             foreach (Pair<string, string> Pair in Parameters)
 126:             {
 127:                 if (Pair.Left == Key)
 128:                 {
 129:                     Pair.Right = Value;
 130:                     Found = true;
 131:                     break;
 132:                 }
 133:             }
 134:             if (!Found)
 135:             {
 136:                 Parameters.Add(new Pair<string, string>(Key, Value));
 137:             }
 138:         }
 139:  
 140:         #endregion
 141:  
 142:         #region Private Functions
 143:  
 144:         /// <summary>
 145:         /// Generates the info used in the signature
 146:         /// </summary>
 147:         /// <param name="UrlString">Url string</param>
 148:         /// <param name="ParameterString">Parameter string</param>
 149:         /// <returns>The base information for the signature</returns>
 150:         private string GenerateBase(out string UrlString, out string ParameterString)
 151:         {
 152:             StringBuilder UrlBuilder = new StringBuilder();
 153:             StringBuilder Builder = new StringBuilder();
 154:             StringBuilder ParameterBuilder = new StringBuilder();
 155:  
 156:             string SignatureMethod = "";
 157:             if (this.SignatureType == Signature.HMACSHA1)
 158:                 SignatureMethod = "HMAC-SHA1";
 159:             else if (this.SignatureType == Signature.RSASHA1)
 160:                 SignatureMethod = "RSA-SHA1";
 161:             else if (this.SignatureType == Signature.PLAINTEXT)
 162:                 SignatureMethod = "PLAINTEXT";
 163:  
 164:             AddParameter("oauth_consumer_key", this.ConsumerKey);
 165:             AddParameter("oauth_nonce", RandomGenerator.Next(123400, 9999999).ToString());
 166:             AddParameter("oauth_signature_method", SignatureMethod);
 167:             AddParameter("oauth_timestamp", Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds).ToString());
 168:             AddParameter("oauth_version", "1.0");
 169:  
 170:             if (!string.IsNullOrEmpty(this.Token))
 171:                 AddParameter("oauth_token", this.Token);
 172:             if (!string.IsNullOrEmpty(this.TokenSecret))
 173:                 AddParameter("oauth_token_secret", this.TokenSecret);
 174:  
 175:             Parameters.Sort(new PairComparer());
 176:  
 177:             string Splitter = "";
 178:             foreach (Pair<string, string> Key in Parameters)
 179:             {
 180:                 ParameterBuilder.Append(Splitter)
 181:                     .Append(Key.Left)
 182:                     .Append("=")
 183:                     .Append(UrlEncode(Key.Right));
 184:                 Splitter = "&";
 185:             }
 186:  
 187:             UrlBuilder.Append(Url.Scheme).Append("://").Append(Url.Host);
 188:             if ((Url.Scheme == "http" && Url.Port != 80) || (Url.Scheme == "https" && Url.Port != 443))
 189:                 UrlBuilder.Append(":").Append(Url.Port);
 190:             UrlBuilder.Append(Url.AbsolutePath);
 191:  
 192:             UrlString = UrlBuilder.ToString();
 193:             ParameterString = ParameterBuilder.ToString();
 194:  
 195:             Builder.Append(this.Method.ToString().ToUpper())
 196:                 .Append("&")
 197:                 .Append(UrlEncode(UrlBuilder.ToString()))
 198:                 .Append("&")
 199:                 .Append(UrlEncode(ParameterBuilder.ToString()));
 200:  
 201:             return Builder.ToString();
 202:         }
 203:  
 204:         #endregion
 205:  
 206:         #region Protected Properties
 207:  
 208:         /// <summary>
 209:         /// Url that is being used
 210:         /// </summary>
 211:         protected Uri Url { get; set; }
 212:  
 213:         /// <summary>
 214:         /// Consumer key
 215:         /// </summary>
 216:         public virtual string ConsumerKey { get; set; }
 217:  
 218:         /// <summary>
 219:         /// Consumer key secret
 220:         /// </summary>
 221:         public virtual string ConsumerKeySecret { get; set; }
 222:  
 223:         /// <summary>
 224:         /// Token
 225:         /// </summary>
 226:         public virtual string Token { get; set; }
 227:  
 228:         /// <summary>
 229:         /// Token secret
 230:         /// </summary>
 231:         public virtual string TokenSecret { get; set; }
 232:  
 233:         /// <summary>
 234:         /// HTTP Method
 235:         /// </summary>
 236:         protected HTTPMethod Method { get; set; }
 237:  
 238:         /// <summary>
 239:         /// The hash type that is suppose to be used
 240:         /// </summary>
 241:         protected Signature SignatureType { get; set; }
 242:  
 243:         #endregion
 244:  
 245:         #region Private Variables
 246:  
 247:         private string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
 248:         private System.Collections.Generic.List<Pair<string, string>> Parameters { get; set; }
 249:         private Random.Random RandomGenerator { get; set; }
 250:  
 251:         #endregion
 252:  
 253:         #region Private Classes
 254:  
 255:         /// <summary>
 256:         /// Comparer class for the pair type
 257:         /// </summary>
 258:         private class PairComparer : IComparer<Pair<string, string>>
 259:         {
 260:             public int Compare(Pair<string, string> x, Pair<string, string> y)
 261:             {
 262:                 if (x.Left == y.Left)
 263:                 {
 264:                     return string.Compare(x.Right, y.Right);
 265:                 }
 266:                 else
 267:                 {
 268:                     return string.Compare(x.Left, y.Left);
 269:                 }
 270:             }
 271:         }
 272:  
 273:         #endregion
 274:     }
 275:  
 276:     #region Enums
 277:  
 278:     /// <summary>
 279:     /// HTTP Method
 280:     /// </summary>
 281:     public enum HTTPMethod
 282:     {
 283:         GET,
 284:         POST,
 285:         DELETE,
 286:         PUT
 287:     }
 288:  
 289:     /// <summary>
 290:     /// Hash type
 291:     /// </summary>
 292:     public enum Signature
 293:     {
 294:         PLAINTEXT,
 295:         RSASHA1,
 296:         HMACSHA1
 297:     }
 298:  
 299:     #endregion
 300: }

The pair class that it uses can be found here:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23:  
  24: #endregion
  25:  
  26: namespace Utilities.DataTypes
  27: {
  28:     /// <summary>
  29:     /// Pairs two items together
  30:     /// </summary>
  31:     /// <typeparam name="T1">Type for the left hand side</typeparam>
  32:     /// <typeparam name="T2">Type for the right hand side</typeparam>
  33:     public class Pair<T1, T2>
  34:     {
  35:         #region Constructor
  36:  
  37:         /// <summary>
  38:         /// Constructor
  39:         /// </summary>
  40:         public Pair()
  41:         {
  42:  
  43:         }
  44:  
  45:         /// <summary>
  46:         /// Constructor
  47:         /// </summary>
  48:         /// <param name="Left">Left hand side of the pair</param>
  49:         /// <param name="Right">Right hand side of the pair</param>
  50:         public Pair(T1 Left, T2 Right)
  51:         {
  52:             this.Left = Left;
  53:             this.Right = Right;
  54:         }
  55:  
  56:         #endregion
  57:  
  58:         #region Properties
  59:  
  60:         /// <summary>
  61:         /// Left hand item
  62:         /// </summary>
  63:         public T1 Left { get; set; }
  64:  
  65:         /// <summary>
  66:         /// Right hand item
  67:         /// </summary>
  68:         public T2 Right { get; set; }
  69:  
  70:         #endregion
  71:  
  72:         #region Public Overridden Properties
  73:  
  74:         public override int GetHashCode()
  75:         {
  76:             if (Left != null && Right != null)
  77:             {
  78:                 return Left.GetHashCode() ^ Right.GetHashCode();
  79:             }
  80:             return 0;
  81:         }
  82:  
  83:         public override bool Equals(object obj)
  84:         {
  85:             if (obj != null && obj is Pair<T1, T2>)
  86:             {
  87:                 return Equals(Left, ((Pair<T1, T2>)obj).Left) && Equals(Right, ((Pair<T1, T2>)obj).Right);
  88:             }
  89:             return false;
  90:         }
  91:  
  92:         #endregion
  93:     }
  94: }

The pair code should be rather straight forward, the OAuth code on the other hand may need some explaining.There is really only one function that you should need to call, GenerateRequest. However prior to calling that, you will need to set up the object (adding the parameters that it needs). Specifically you need to add the url, signature type (hashing type), HTTP method (GET, POST, DELETE, PUT), as well as the consumer key and consumer key secret. The consumer key and consumer key secret are supplied by the service provider (in my case Netflix). The signature type (the only other parameter that you may have questions with), is really the hashing algorithm that should be used. OAuth uses three different options when hashing, HMAC-SHA1, RSA-SHA1, and plain text. HMAC-SHA1 is what Netflix uses and thus I put in the code for that along with plain text for testing purposes. RSA-SHA1 isn't implemented but it should be fairly easy to add

There are a couple quirks though when dealing with OAuth. First, the query string needs to be sorted by the key. On top of that, since it is simply an url that you will be getting back, it needs to use url encoding. However you will notice that I'm not using the built in function in .Net. The reason for this is the simple fact that the .Net implementation uses lower case and OAuth seems to require upper case. That's it though (and the code above takes care of all of that). All you need to do is inherit from the base class here and set things up. So try it out, leave feedback, and happy coding.



Comments

James Craig
May 18, 2011 7:30 PM

You should just be able to create an HttpWebRequest object, set the method to post, and the Method property on the OAuth class to POST, and you should be good to go (from my experience anyway, but my experience is limited to Netflix and Twitter).

Scott
May 16, 2011 7:50 AM
How do you handle a true POST call? i.e. specifying a post url and then the body data in relation to the signature?