Skip to content

Instantly share code, notes, and snippets.

@Redth
Created June 1, 2011 17:16
Show Gist options
  • Save Redth/1002789 to your computer and use it in GitHub Desktop.
Save Redth/1002789 to your computer and use it in GitHub Desktop.
Google 3-Legged OAuth C#
/*
* Google OAuth 3-Legged for C#
*
* Author: Redth
* Date: June 1, 2011
*
* This class if for Authenticating to Google via 3-Legged OAuth
*
* Example Use:
*
* var goauth = new GoogleOAuthStep("my-app", "My Application",
* true, null, null, null, "emailofuserauthenticating@gmail.com",
* "https://www.google.com/m8/feeds/"); //Scope used here is google contacts
*
* goauth.OnUserAuthorizationPrompt += delegate(string url) {
* // Launch the url with the default system browser
* System.Diagnostics.Process.Start(url);
*
* Console.WriteLine("In the browser that opened, Grant Access to the Application");
* Console.WriteLine("Next, type in the Verification Code...:");
*
* //Get the verification code from the user
* return Console.ReadLine();
* };
*
* goauth.Authorize();
*
* Console.WriteLine("Access Token: " + goauth.Token);
* Console.WriteLine("Access Token Secret: " + goauth.TokenSecret);
*
* var valid = goauth.ValidateTokens(goauth.Token, goauth.TokenSecret);
*
* Console.WriteLine("Tokens Valid? " + valid.ToString());
*
*
*
* Notes:
* 1. Obviously you will want to get a bit more fancy on how you handle the
* OnUserAuthorizationPrompt event.
* 2. If you specify a Callback Url to a page on your server it's easy enough
* to parse out the request variables that google will send along to extract
* the tokens. Then you can have your client automatically detect the presence
* of those tokens instead of making the user type them in like in the example.
* 3. If you register your application with Google, you will need to supply a different
* Consumer Key and Consumer Secret that Google gives you. In the Example, we use
* null which gets replaced with "anonymous" as per Google's documentation.
* 4. We also use null for Callback Url which gets replaced with 'oob' as per google's
* documentation. This basically means google shows you the verification code on their
* own page.
*
*/
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Net;
namespace GoogleOAuth
{
public enum GoogleOAuthStep
{
GetRequestToken,
AuthorizeToken,
GetAccessToken,
ValidateTokens
}
public class GoogleOAuth
{
public delegate string UserAuthorizationPromptDelegate(string url);
public event UserAuthorizationPromptDelegate OnUserAuthorizationPrompt;
const string OAuthGetRequestTokenUrl = "https://www.google.com/accounts/OAuthGetRequestToken";
const string OAuthAuthorizeToken = "https://www.google.com/accounts/OAuthAuthorizeToken";
const string OAuthGetAccessToken = "https://www.google.com/accounts/OAuthGetAccessToken";
const string OAuthVerifyTokens = "https://www.google.com/accounts/AuthSubTokenInfo";
public GoogleOAuth(string appName,
string displayName,
bool mobile,
string oauthConsumerKey,
string oauthConsumerSecret,
string oauthCallbackUrl,
string email,
params string[] scopes)
{
this.Step = GoogleOAuthStep.GetRequestToken;
this.LastError = string.Empty;
this.Scopes = scopes;
this.Email = email;
this.DisplayName = displayName;
this.Mobile = mobile;
this.ConsumerKey = string.IsNullOrEmpty(oauthConsumerKey) ? "anonymous" : oauthConsumerKey;
this.ConsumerSecret = string.IsNullOrEmpty(oauthConsumerSecret) ? "anonymous" : oauthConsumerSecret;
this.CallbackUrl = string.IsNullOrEmpty(oauthCallbackUrl) ? "oob" : oauthCallbackUrl;
}
public GoogleOAuthStep Step
{
get;
private set;
}
public string LastError
{
get;
private set;
}
public string ApplicationName
{
get;
private set;
}
public string DisplayName
{
get;
private set;
}
public bool Mobile
{
get;
private set;
}
public string Email
{
get;
private set;
}
public string CallbackUrl
{
get;
private set;
}
public string ConsumerKey
{
get;
private set;
}
public string ConsumerSecret
{
get;
private set;
}
public string Token
{
get;
private set;
}
public string TokenSecret
{
get;
private set;
}
public string[] Scopes
{
get;
private set;
}
/// <summary>
/// Validates the given token and tokenSecret to ensure it is still valid for the given scopes
/// </summary>
/// <param name="token">Access Token returned from Authorization</param>
/// <param name="tokenSecret">Access Token Secret returned from Authorization</param>
/// <returns>True if the Token is still valid and is valid for the given scopes</returns>
public bool ValidateTokens(string token, string tokenSecret)
{
//This is largely unadvertised as a means to validate OAuth Token and token scope.
// This is the documented way to get AuthSub info, but it works for OAuth too!
this.Step = GoogleOAuthStep.ValidateTokens;
//Important that these parameters are in alpha order
var p = new NameValueCollection();
p.Add("oauth_consumer_key", this.ConsumerKey);
p.Add("oauth_nonce", RandomInt64().ToString());
p.Add("oauth_signature_method", "HMAC-SHA1");
p.Add("oauth_timestamp", EpochNow().ToString());
p.Add("oauth_token", token);
p.Add("oauth_version", "1.0");
//Build the signature
p.Add("oauth_signature", GenerateSignature(OAuthVerifyTokens, p, this.ConsumerSecret, tokenSecret));
//Get a response
var url = BuildUrl(OAuthVerifyTokens, p);
var data = DownloadUrl(url);
//The respone from google comes in lines
var lines = data.Split('\n');
//No lines parsed? had an issue
if (lines == null || lines.Length <= 0)
{
this.LastError = data;
return false;
}
//We want to find all the valid scopes returned
// eg format: Scope=...\nScope2=... etc.
var validScopes = new List<string>();
//There will be a line Secure=true if the token is valid still
bool secure = false;
//Parse out the lines
foreach (var line in lines)
{
if (line.StartsWith("Scope", StringComparison.InvariantCultureIgnoreCase)
&& line.Contains('='))
{
var scope = line.Substring(line.IndexOf('=') + 1);
if (!string.IsNullOrEmpty(scope))
validScopes.Add(scope);
}
else if (line.StartsWith("Secure=true", StringComparison.InvariantCultureIgnoreCase))
secure = true;
}
//Find if any required scopes are missing from the valid scopes
var missingScopes = from s in this.Scopes
where !validScopes.Exists(vs => vs.Equals(s, StringComparison.InvariantCultureIgnoreCase))
select s;
if (missingScopes.Count() > 0)
{
this.LastError = "Missing Scopes:" + Environment.NewLine + string.Join(Environment.NewLine, missingScopes);
return false;
}
if (!secure)
{
this.LastError = "Not Secured: " + data;
return false;
}
return true;
}
/// <summary>
/// Authorizes an account with OAuth for the specified Google scopes
/// </summary>
/// <returns>True if Authorization Succeeded, with the Token and TokenSecret properties populated</returns>
public bool Authorize()
{
//Step 1: Get Request Token
// IMPORTANT NOTE: For the GenerateSignature to work properly all the parameters in this
// collection must be in alpha order!!!
var p = new NameValueCollection();
p.Add("oauth_callback", this.CallbackUrl);
p.Add("oauth_consumer_key", this.ConsumerKey);
p.Add("oauth_nonce", RandomInt64().ToString());
p.Add("oauth_signature_method", "HMAC-SHA1");
p.Add("oauth_timestamp", EpochNow().ToString());
p.Add("oauth_version", "1.0");
p.Add("scope", string.Join(" ", Scopes));
p.Add("xoauth_displayname", DisplayName);
//Add the last paramaeter which uses the existing ones to generate a signature
p.Add("oauth_signature", GenerateSignature(OAuthGetRequestTokenUrl, p, this.ConsumerSecret, null));
//Build the url and download the data
var url = BuildUrl(OAuthGetRequestTokenUrl, p);
var data = DownloadUrl(url);
var responseParameters = ParseQueryString(data);
//Parse out the tokens in the response
this.Token = responseParameters["oauth_token"] ?? "";
this.TokenSecret = responseParameters["oauth_token_secret"] ?? "";
//If the tokens aren't there, we had an issue
if (string.IsNullOrEmpty(this.Token)
|| string.IsNullOrEmpty(this.TokenSecret))
{
this.LastError = data;
return false;
}
//Step 2: Authorize the Token
this.Step = GoogleOAuthStep.AuthorizeToken;
//Build the url to show the user
url = string.Format("{0}?oauth_token={1}", OAuthAuthorizeToken, this.Token);
//Mobile support can be forced
if (Mobile)
url += "&btmpl=mobile";
//The consumer of this class must handle this event
// They should show the user the url and get the verification code and return it
string verifier = this.OnUserAuthorizationPrompt(url);
if (string.IsNullOrEmpty(verifier))
{
this.LastError = "Missing Verifier!";
return false;
}
//Step 3: Get Access Token
this.Step = GoogleOAuthStep.GetAccessToken;
//Again make sure these are in alpha order
p.Clear();
p.Add("oauth_consumer_key", this.ConsumerKey);
p.Add("oauth_nonce", RandomInt64().ToString());
p.Add("oauth_signature_method", "HMAC-SHA1");
p.Add("oauth_timestamp", EpochNow().ToString());
p.Add("oauth_token", this.Token);
p.Add("oauth_verifier", verifier);
p.Add("oauth_version", "1.0");
//Generating the signature, this time we have a TokenSecret we must include
p.Add("oauth_signature", GenerateSignature(OAuthGetAccessToken, p, this.ConsumerSecret, this.TokenSecret));
//Get the response
url = BuildUrl(OAuthGetAccessToken, p);
data = DownloadUrl(url);
responseParameters = ParseQueryString(data);
//Parse out the tokens in the response
this.Token = responseParameters["oauth_token"] ?? "";
this.TokenSecret = responseParameters["oauth_token_secret"] ?? "";
//If we have no tokens, we had an issue
if (string.IsNullOrEmpty(this.Token) || string.IsNullOrEmpty(this.TokenSecret))
{
this.LastError = data;
return false;
}
//Everything went ok!
return true;
}
string DownloadUrl(string url)
{
var wc = new WebClient();
var data = string.Empty;
try { data = wc.DownloadString(url); }
catch (WebException wex)
{
try
{
using (var sr = new System.IO.StreamReader(wex.Response.GetResponseStream()))
{
data = sr.ReadToEnd();
}
}
catch { }
}
return data;
}
string BuildUrl(string baseUrl, NameValueCollection param)
{
var url = new StringBuilder();
url.Append(baseUrl);
url.Append("?");
foreach (var key in param.AllKeys)
url.AppendFormat("{0}={1}&", key, UrlEncode(param[key]));
if (url.Length > 1)
url.Remove(url.Length - 1, 1);
return url.ToString();
}
string GenerateSignature(string baseUrl, NameValueCollection param, string consumerSecret, string tokenSecret)
{
var pStr = new StringBuilder();
foreach (var key in param.AllKeys)
pStr.AppendFormat("{0}={1}&", key, UrlEncode(param[key]));
if (pStr.Length > 1) //Remove trailing &
pStr.Remove(pStr.Length - 1, 1);
var baseStr = string.Format("GET&{0}&{1}",
UrlEncode(baseUrl),
UrlEncode(pStr.ToString()));
HMACSHA1 sha1 = new HMACSHA1();
sha1.Key = Encoding.ASCII.GetBytes(UrlEncode(consumerSecret) + "&" + (string.IsNullOrEmpty(tokenSecret) ? "" : UrlEncode(tokenSecret)));
return Convert.ToBase64String(sha1.ComputeHash(System.Text.Encoding.ASCII.GetBytes(baseStr)));
}
public string UrlEncode(string Input)
{
StringBuilder Result = new StringBuilder();
for (int x = 0; x < Input.Length; ++x)
{
if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"
.IndexOf(Input[x]) != -1)
Result.Append(Input[x]);
else
Result.Append("%").Append(String.Format("{0:X2}", (int)Input[x]));
}
return Result.ToString();
}
public string UrlDecode(string data)
{
var result = data;
var rxUrl = new Regex("%[A-Z0-9]{2}", RegexOptions.IgnoreCase | RegexOptions.Singleline);
var matches = rxUrl.Matches(data);
foreach (Match m in matches)
{
var hex = m.Value.TrimStart('%');
if (m.Success)
result = result.Replace(m.Value, new string((char)int.Parse(hex, System.Globalization.NumberStyles.HexNumber), 1));
}
return result;
}
ulong RandomInt64()
{
var rnd = new Random();
var buffer = new byte[sizeof(ulong)];
rnd.NextBytes(buffer);
return BitConverter.ToUInt64(buffer, 0);
}
ulong EpochNow()
{
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return (ulong)(DateTime.UtcNow - epoch).TotalSeconds;
}
NameValueCollection ParseQueryString(string data)
{
var results = new NameValueCollection();
if (data.Contains('?'))
data = data.Substring(data.IndexOf('?') + 1);
var fields = data.Split('&');
if (fields == null)
return results;
foreach (var field in fields)
{
var parts = field.Split(new char[] { '=' }, 2);
if (parts != null && parts.Length >= 1)
results.Add(parts[0], parts.Length == 2 ? UrlDecode(parts[1]) : "");
}
return results;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment