Click here to Skip to main content
Click here to Skip to main content
Go to top

Demystifying OAuth

, 30 Nov 2010
Rate this:
Please Sign up or sign in to vote.
An attempt to know under-the-hood of Open Authorization protocol

Introduction

I'm a great fan of social networking sites and especially Twitter. Since the time when I came to know about it, I started building custom applications for my laptop, mobile phone and set-top box according to my preferences and taste. Being a C# .NET developer, it was so easy to consume Twitter APIs in my custom-made applications. Suddenly, Twitter planned to suspend Basic Authentication for their APIs and planned to move on with OAuth authentication which would leave my application unusable. This urged me to drive into knowing about OAuth and I was trying to figure it out to implement in my applications which I'd like to share over here in this article. Thanks to Eran Hammer-Lahav and Shannon Whitley for their intuitive articles on which this article is based on. Most of the articles that I came across give protocol specification or the implementation details, I'd like to take through a mapping of both.

Background

OAuth, I'd say is a Valet Key of Web. I'd assume that the concept is taken from automobile industry and it does a perfect analogy. Cars come with a valet key. It is a special key you give the parking attendant and unlike your regular key, will not allow the car to drive more than a mile or two. Some valet keys will not open the trunk, while others will block access to your onboard cell phone address book. Regardless of what restrictions the valet key imposes, the idea is very clever. You give someone limited access to your car with a special key, while using your regular key to unlock everything. That's exactly the idea behind OAuth. It allows you as an user to grant access to your private resources on web (Service Provider) to another application (Consumer) so that the consumer acts on behalf of the user when accessing the service provider.

Terminologies

Token – A string of hashed data given to you as a unique ID to identify your application, and the user trying to use your application by the Service Provider.

Request Token – The token that the Consumer gets from the Service Provider before redirecting the User to authenticate with the Service Provider. If the User’s authentication is successful, the Service Provider creates an access token which identifies the User and associates them with the Consumer. The Consumer can then access that access token later by sending another request after the User has authenticated with the Request Token and the Request Token Secret Key.

Request Token Secret – A string of hashed text, which only the application developer will ever see or use. The Consumer retrieves this when it gets the Request Token, and will need to pass it with the Request Token when it requests to get an Access Token. Consider this the password when trying to get an Access Token. The Request Token is the ID for the Service Provider to identify the Consumer request with to verify the User authenticated successfully and the Consumer has permission to access the Service Provider.

Access Token – Once the Consumer has the Request Token, Request Token Secret, and the User has authenticated successfully, and assuming the Consumer has been given permission by the Service Provider to access its API, the Consumer can then make requests to Service Provider to get a Access Token. The Consumer sends Service Provider the User's Request Token and Request Token Secret, and the response returns the User's Access Token and an Access Token Secret Key for accessing Service Provider's API. After the Consumer has the User's Access Token, it can make requests to Service Provider on behalf of the User by sending User's Access Token and Access Token Secret with the request.

Access Token Secret – The secret key to pass with an Access Token when making API calls to Service Provider. Consider this the password that goes along with the ID, which is the Access Token. The Service Provider looks up the Access Token ID, verifies the user is authenticated, and then checks that you also have a valid Access Token Secret Key. If both are correct and valid, the Service Provider will send back the data needed to access its API.

Consumer Key – The unique ID of the Consumer as identified by Service Provider. The Consumer uses this when asking for User's Request Token.

Consumer Key Secret – The password to go with Consumer Key when asking for User's Request Token from Service Provider. The Service Provider looks up Consumer by its ID (Consumer Key), and verifies by checking Consumer Key Secret.

Signature - OAuth uses digital signatures instead of sending the full credentials (specifically, passwords) with each request. OAuth defines 3 signature methods used to sign and verify requests: PLAINTEXT, HMAC-SHA1, and RSA-SHA1. PLAINTEXT is intended to work over HTTPS and in a similar fashion to how HTTP ‘Basic’ transmits the credentials unencrypted. Unlike ‘Basic’, PLAINTEXT supports delegation. The other two methods use the HMAC and RSA signature algorithm combined with the SHA1 hash method.

Signature Base - Consumers and Service Providers must perform signature process in an identical manner in order to produce the same result. Not only must they both use the same algorithm and shared secret, but also, they must sign the same content. This requires a consistent method for converting HTTP requests into a single string which is used as the signed content — Signature Base string.

Nonce - Number Used Once. It is a unique and usually random string that is meant to uniquely identify each signed request. By having a unique identifier for each request, the Service Provider is able to prevent requests from being used more than once. This means the Consumer generates a unique string for each request sent to the Service Provider, and the Service Provider keeps track of all the nonces used to prevent them from being used a second time. Since the nonce value is included in the signature, it cannot be changed by an attacker without knowing the shared secret.

Timestamp - Using nonces can be very costly for Service Providers as they demand persistent storage of all nonce values received, ever. To make implementations easier, OAuth adds a timestamp value to each request which allows the Service Provider to only keep nonce values for a limited time. When a request comes in with a timestamp that is older than the retained time frame, it is rejected as the Service Provider no longer has nonces from that time period. It is safe to assume that a request sent after the allowed time limit is a replay attack.

Workflow

  1. Consumer registers with Service Provider and obtains its Consumer Key and Consumer Secret.
  2. When the User wants the Consumer to act on its behalf with the Service Provider, the Consumer initiates a OAuth transaction by sending its Consumer Key.
  3. The Service Provider identifies the Consumer with its Consumer Key and sends back a Request Token.
  4. The Consumer exchanges Request Token with Access Token after redirecting the User to the Service Provider web site for authorization. User can choose to approve/deny the Consumer authorization request.\
  5. Consumer uses Access Token to access designated protected resource of the User and acts on their behalf.

Signing Requests

Step 1: The OAuth information such as Tokens are included in the request using special OAuth parameters starting with 'oauth_' prefix. OAuth does not allow any other parameter to use 'oauth_' prefix.

Step 2: OAuth parameters and request parameters are collected together in their raw, pre-encoded form. The parameters are collected from three locations: URL query element, OAuth 'Authorization' header (excluding 'realm' parameter), and parameters included in a single-part 'application/x-www-form-urlencoded' POST body. The parameter locations are more relevant to the Service Provider as it needs to extract them from the incoming Consumer request. The Consumer should have all the parameters in their separated and pre-encoded form as it builds the request.

Step 3: All text parameters are UTF-8 encoded. Binary data is not directly handled by the OAuth specification but is assumed to be stored in an 8bit array which is not UTF-8 encoded. This step may not have any effect if the parameters are only using the ASCII character set.

Step 4: After UTF-8 encoding, the parameters are URL-encoded in a specific way that is often not fully compatible with existing URL-encoding libraries. All unreserved characters (letters, numbers, '-', '_', '.', '~') must not be encoded, while all other characters are encoded using the %XX format where XX is an uppercase representation of the character hexadecimal value.

public string UrlEncode(string value)
{
    Console.WriteLine("Encoding URL...");
    
    //This is a different Url Encode implementation since the default .NET one outputs 
    //the percent encoding in lower case. While this is not a problem with the percent 
    //encoding spec, it is used in upper case throughout OAuth
    
    string unreserved = 
	"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
    
    StringBuilder result = new StringBuilder();
    
    foreach (char symbol in value)
    {
        if (unreserved.IndexOf(symbol) != -1)
            result.Append(symbol);
        else
            result.Append('%' + String.Format("{0:X2}", (int)symbol));
    }
    
    Console.WriteLine("URL encoded. " + result.ToString());
    
    return result.ToString();
}

Step 5: The parameters are sorted first based on their encoded names, and if equal, based on their encoded values. Sort order is lexicographical byte value ordering which is the default string sort method in most languages, and means comparing the byte value of each character and sorting in an ascending order (which results in a case sensitive sort). It is important not to try and perform the sort operation on some combined string of both name and value as some known separators (such as '=') will cause the sort order to change due to their impact on the string value.

protected class QueryParameterComparer : IComparer
{
    // used to perform sorting of the query parameters
    public int Compare(QueryParameter x, QueryParameter y)
    {
        if (x.Name == y.Name)
            return string.Compare(x.Value, y.Value);
        else
            return string.Compare(x.Name, y.Name);
    }
}

Step 6: Once encoded and sorted, the parameters are concatenated together into a single string. Each parameter's name is separated from the corresponding value by an ‘=’ character (even if the value is empty), and each name-value pair is separated by an ‘&’ character. This method is similar to how HTML form data is encoded in 'application/x-www-form-urlencoded' but due to the specific encoding and sorting requirements, is often not fully compatible with existing libraries.

protected string NormalizeParameters(IList parameters)
{
    Console.WriteLine("Normalizing request parameters...");
    
    StringBuilder result = new StringBuilder();
    
    QueryParameter p = null;
    
    for (int i = 0; i < parameters.Count; i++)
    {
        p = parameters[i];
        
        result.AppendFormat("{0}={1}", p.Name, p.Value);
        
        if (i < parameters.Count - 1)
            result.Append("&");
    }
    
    Console.WriteLine("Request parameter normalized. " + result.ToString());
    
    return result.ToString();
}

Step 7: After the parameters have been normalized, the other request elements are processed. While the web is built on URLs, the HTTP protocol uses the data in pieces. URLs are constructed from various elements, and take the general form of scheme://authority:port/path?query#fragment. The scheme and port are used to establish the desired connection, the authority is used in the 'Host' header and to connect to the Service Provider, and the path and query parts are used in the request itself (the fragment is used locally by the browser and is not transmitted with the request).

Step 8: The request URL is normalized as scheme://authority:port/path as the query is already included in the list of parameters and the fragment is excluded. The scheme and authority must be in lowercase, and the port must be present except for the default HTTP(S) ports which must be omitted ('80' is omitted when the scheme is 'http' and '443' is omitted when the scheme is 'https'). The path must retain its case as some platforms are case-sensitive. The HTTP request should include the 'Host' header with the exact same host name used to create the normalized URL string. Developers should verify with the Service Provider that it does not require any special handling of the URL.

Step 9: To complete the creation of the Signature Base String — the input to the signature algorithm — all the request pieces must be put together into a single string. The HTTP method (such as GET, POST, etc.) which is a critical part of HTTP requests is concatenated together with the normalized URL and normalized parameters. The HTTP method must be in uppercase and each of these three pieces is URL-encoded (as defined above) and separated by an '&'.

protected string GenerateSignatureBase(Uri url, string consumerKey, string token, 
    string tokenSecret, string callbackUrl, string oauthVerifier, string httpMethod, 
    string timestamp, string nonce, string signatureType, 
    out string normalizedUrl, out string normalizedRequestParameters)
{
    Console.WriteLine("Generating signature base...");
    
    if (token == null)
        token = string.Empty;
        
    if (tokenSecret == null)
        tokenSecret = string.Empty;
        
    if (string.IsNullOrEmpty(consumerKey))
        throw new ArgumentNullException("Consumer Key");
        
    if (string.IsNullOrEmpty(httpMethod))
        throw new ArgumentNullException("HTTP Method");
        
    if (string.IsNullOrEmpty(signatureType))
        throw new ArgumentNullException("Signature Type");
        
    normalizedUrl = null;
    normalizedRequestParameters = null;
    
    List parameters = GetQueryParameters(url.Query);
    
    parameters.Add(new QueryParameter(OAUTH_VERSION, OAuthVersion));
    parameters.Add(new QueryParameter(OAUTH_NONCE, nonce));
    parameters.Add(new QueryParameter(OAUTH_TIMESTAMP, timestamp));
    parameters.Add(new QueryParameter(OAUTH_SIGNATURE_METHOD, signatureType));
    parameters.Add(new QueryParameter(OAUTH_CONSUMER_KEY, consumerKey));
    
    if (!string.IsNullOrEmpty(callbackUrl))
        parameters.Add(new QueryParameter(OAUTH_CALLBACK, UrlEncode(callbackUrl)));
        
    if (!string.IsNullOrEmpty(oauthVerifier))
        parameters.Add(new QueryParameter(OAUTH_VERIFIER, oauthVerifier));
        
    if (!string.IsNullOrEmpty(token))
        parameters.Add(new QueryParameter(OAUTH_TOKEN, token));
        
    parameters.Sort(new QueryParameterComparer());
    
    normalizedUrl = string.Format("{0}://{1}", url.Scheme, url.Host);
    if (!((url.Scheme == "http" && url.Port == 80) || 
    (url.Scheme == "https" && url.Port == 443)))
        normalizedUrl += ":" + url.Port;
        
    normalizedUrl += url.AbsolutePath;
    normalizedRequestParameters = NormalizeParameters(parameters);
    
    StringBuilder signatureBase = new StringBuilder();
    signatureBase.AppendFormat("{0}&", httpMethod.ToUpper());
    signatureBase.AppendFormat("{0}&", UrlEncode(normalizedUrl));
    signatureBase.AppendFormat("{0}", UrlEncode(normalizedRequestParameters));
    
    Console.WriteLine("Signature base generated. " + signatureBase.ToString());
    
    return signatureBase.ToString();
}

Step 10: The HMAC-SHA1 signature method uses the two secrets — Consumer Secret and Token Secret — as the HMAC-SHA1 algorithm key. To construct the key, each secret is UTF8-encoded, URL-encoded, and concatenated into a single string using an '&' character as separator even if either secret is empty.

Step 11: With the Signature Base String as the HMAC-SHA1 text and concatenated secrets as key, the Consumer generates the signature. The HMAC-SHA1 algorithm will generate an octet string as the result. The octet string must be base64-encoded with '=' padding:

protected string ComputeHash(HashAlgorithm hash, string data)
{
    Console.WriteLine("Computing hash...");
    
    string result = string.Empty;
    
    if (hash == null)
        throw new ArgumentNullException("hash");
        
    if (string.IsNullOrEmpty(data))
        throw new ArgumentNullException("data");
        
    byte[] input = Encoding.ASCII.GetBytes(data);
    byte[] output = hash.ComputeHash(input);
    
    result = Convert.ToBase64String(output);
    
    Console.WriteLine("Hash computed. " + result);
    
    return result;
}

Step 12: The calculated signature is added to the request using the 'oauth_signature' parameter. When the signature is verified by the Service Provider, this parameter is not included in the signature workflow as it was not part of the Signature Base String signed by the Consumer. When the signature is included in the HTTP request, it must be properly encoded as required by the method used to transmit the parameters.

OAuth does not directly specify how the request itself should be made and how the parameters should be delivered. But since it explicitly defines which parameters are included in the signature for verification by the Service Provider, it implicitly defines where parameters should be included in the request. OAuth Parameters can be included in either one (even simultaneously) of three locations: the URL query element, the OAuth 'Authorization' header, or in a single-part 'application/x-www-form-urlencoded' POST body (as defined by HTML4). Signed non-OAuth Parameters can be included in one or both of these locations: the URL query element or in a single-part 'application/x-www-form-urlencoded' POST body. It is highly recommended that whenever possible, the OAuth Parameters be included in the OAuth 'Authorization' header and that no other parameters will be included in that header.

Using the Code

The sample code below demonstrates the usage of OAuth test harness in a console application.

using System;
using System.Net;
using System.Net.Security;
using System.Windows.Forms;
using System.Security.Cryptography.X509Certificates;

namespace TwitterOAuthTestHarness
{
    public class Test
    {
        private const string REQUEST_TOKEN_URL = 
		"https://api.twitter.com/oauth/request_token";

        private const string ACCESS_TOKEN_URL = 
		"https://api.twitter.com/oauth/access_token";

        private const string AUTHORIZE_URL = "https://api.8/1/2010.com/oauth/authorize";

        private const string CONSUMER_KEY = "Your Application's Consumer Key Here";

        private const string CONSUMER_SECRET = "Your Application's Consumer Secret Here";

        public bool CertValidationCallback
	(object sender, X509Certificate certificate, X509Chain chain, 
	SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }

        public bool Execute()
        {
            ServicePointManager.ServerCertificateValidationCallback += 
		new RemoteCertificateValidationCallback(CertValidationCallback);

            bool result = false;

            TwitterAuth auth = new TwitterAuth();
            auth.ConsumerKey = CONSUMER_KEY;
            auth.ConsumerSecret = CONSUMER_SECRET;

            AuthWindow ie = new AuthWindow(auth.GetAuthorizationUrl());
            if (ie.ShowDialog() == DialogResult.OK)
            {
                auth.OAuthVerifier = ie.Pin;
                auth.GetAccessToken();

                result = true;
                
                // twitter status update test
                string url = "http://twitter.com/statuses/update.xml";
                string xml = auth.ExecuteOAuth(TwitterAuth.Method.POST, url, 
		"status=" + auth.UrlEncode
		("Hello World! - Testing Twitter OAuth from C#."));                
                if (!string.IsNullOrEmpty(xml))
                    Console.WriteLine(xml);
            }

            return result;
        }
    }
}

Digging Deep

How can this authorization protocol be realized in applications running on non-browser environments, where a redirection is not possible??? I'm currently working on this aspect. Guys, please feel free to share your thoughts.

References

History

  • August 01, 2010 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Paramesh Gunasekaran
Technical Lead HCL Technologies
India India
Paramesh Gunasekaran is currently working as a Software Engineer in HCL Technologies, India. He obtained his Bachelor's degree in Information Technology from Anna University, India. His research areas include Computational Biology, Artificial Neural Networks and Network Engineering. He has also received international acclaim for authoring industry papers in these areas. He is a Microsoft Certified Professional in ASP.NET/C# and has also been working in .NET technologies for more than 8 years.
 
Web: http://www.paramg.com

Comments and Discussions

 
QuestionURL REDIRECTION PinmemberMember 917112831-Jul-12 20:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140926.1 | Last Updated 30 Nov 2010
Article Copyright 2010 by Paramesh Gunasekaran
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid