I've been banging my head against the desk for about a whole week trying to make the BitBucket OAuth API work and I can't even get a single request token for saving my own life. The code I made here works with LinkedIn, but BitBucket always returns HTTP status code 400 with a very verbose and helpful message: "Could not verify OAuth request".
It's a simple code (50 lines, couldn't make it shorter) and it is that way because I need to implement OAuth manually, else I wouldn't be asking here and using other external libraries but due to company's requirement I'm not allowed to use external libs in this project. I don't see what's wrong though, 1.0a isn't that hard and getting a request token shouldn't take this long. What could be wrong?
I've also checked my timestamp and it's good, w32tm.exe against pool.ntp.org returns the time with a +30 or something. I've also tried adding and removing 30 minutes to the UtcNow timestamp without success, but my clock is correctly synced (with local time and correct GMT values (GMT -4:30)) so it makes no sense at all.
Could it be because I'm behind a company firewall (Forefront)? but then, why does LinkedIn's API calls work and BitBucket don't? I've also read a lot of documents such as the OAuth Bible, the RFCs, official documents, etc. And of course made an extensive search on SO before asking and looked at all the links shown in the "Similar Questions" panel before hitting the "Post your question" button.
Here's the simple code (C#):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Security.Cryptography;
namespace OAuthTest1
{
class Program
{
static void Main(string[] args)
{
String key = "KEY";
String secret = "SECRET";
String requestUrl = "https://bitbucket.org/api/1.0/oauth/request_token";
string sigBaseStringParams = "oauth_consumer_key=" + key;
sigBaseStringParams += "&" + "oauth_nonce=" + GetNonce();
sigBaseStringParams += "&" + "oauth_signature_method=" + "HMAC-SHA1";
sigBaseStringParams += "&" + "oauth_timestamp=" + GetTimeStamp();
sigBaseStringParams += "&" + "oauth_callback=http%3A%2F%2Flocal%3Fdump";
sigBaseStringParams += "&" + "oauth_version=1.0";
string sigBaseString = "POST&";
sigBaseString += Uri.EscapeDataString(requestUrl) + "&" + Uri.EscapeDataString(sigBaseStringParams);
string signature = GetSignature(sigBaseString, secret);
Console.WriteLine(PostData(requestUrl, sigBaseStringParams + "&oauth_signature=" + Uri.EscapeDataString(signature)));
Console.ReadKey();
}
public static string GetNonce()
{
return new Random().Next(1000000000).ToString();
}
public static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
public static String PostData(string url, string postData)
{
WebClient w = new WebClient();
return w.UploadString(url, postData);
}
public static string GetSignature(string sigBaseString, string consumerSecretKey, string requestTokenSecretKey = null)
{
HMACSHA1 hmacsha1 = new HMACSHA1();
String signingKey = string.Format("{0}&{1}", consumerSecretKey, !string.IsNullOrEmpty(requestTokenSecretKey) ? requestTokenSecretKey : "");
hmacsha1.Key = Encoding.UTF8.GetBytes(signingKey);
return Convert.ToBase64String(hmacsha1.ComputeHash(Encoding.UTF8.GetBytes(sigBaseString)));
}
}
}
Edit: Here's a fiddler and browser capture by request:
Thanks in advance!
Edit 2: I made a new test, using the OAuthBase.cs class provided by n0741337's suggestion, however, that class does not comply with the 1.0A spec where it asks for a callback method (which at least Bitbucket is kind enough to say that such parameter is required) so I had to modify it so it included the callback parameter in the base signature string (in raw format without encoding). Same results though (I figured it doesn't matter if I show my key since you won't be seeing my secret key anyways), here's a capture:
And here's the signature base string:
GET&https%3A%2F%2Fbitbucket.org%2Fapi%2F1.0%2Foauth%2Frequest_token&oauth_callback%3Dhttp%3A%2F%2Flocalhost%3A4000%2F%26oauth_consumer_key%3Dkey%26oauth_nonce%3D8231861%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1423671576%26oauth_version%3D1.0
Also, to put my code in doubt, one can also use this class I found which someone already adapted for Twitter (though Twitter is blocked in my workplace so I can't test against it), but the results are the same.
Here's the new code I made which uses such classes:
using System;
using System.Net;
using System.Security.Cryptography;
using OAuth;
namespace OAuthTest1
{
public class oauth2
{
public static void Run()
{
OAuthBase a = new OAuthBase();
String nonce = a.GenerateNonce();
String ts = a.GenerateTimeStamp();
String key = "key";
String secret = "secret";
String requestUrl = "https://bitbucket.org/api/1.0/oauth/request_token";
String normalizedUrl, normalizedArgs;
String sigBase = a.GenerateSignatureBase(
new Uri(requestUrl),
key,
null,
secret,
"http://localhost:4000/",
"GET",
ts,
nonce,
"HMAC-SHA1",
out normalizedUrl,
out normalizedArgs
);
String sig = a.GenerateSignatureUsingHash(sigBase, new HMACSHA1());
String GETArgs = String.Empty;
GETArgs += "oauth_consumer_key=" + key;
GETArgs += "&oauth_nonce=" + nonce;
GETArgs += "&oauth_signature_method=HMAC-SHA1";
GETArgs += "&oauth_timestamp=" + ts;
GETArgs += "&oauth_version=1.0";
GETArgs += "&oauth_callback=" + a.UrlEncode("http://localhost:4000/");
GETArgs += "&oauth_signature=" + sig;
WebClient w = new WebClient();
Console.WriteLine(w.DownloadString(requestUrl + "?" + GETArgs));
Console.ReadKey();
}
}
}
Question for Edit 2: Does anyone know of an app which connects to Bitbucket's service through this method so I can run Fiddler and see what it is sending? If I could see some output I could at least replicate the flow :/ I've tried against SourceTree but it doesn't work very well.
Edit 3: By AZ.'s suggestion I changed the timestamp generation code with this, but it doesn't work anyways :(. Timestamp values look okay though, there's only a slight difference of 5 seconds between my timestamp and the server's:
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
String timeStamp = Convert.ToInt64(ts.TotalSeconds).ToString();
Also, I notice the signature contains a "+" which should've been encoded to %20, which I did when I noticed this after editing the question and it doesn't work either, just FYI.
There are 2 things wrong with your program:
Content-Type: application/x-www-form-urlencoded
header, which makes the POST body unintelligible (it's generally recommended to use the Authorization request header).oauth_callback
you got the ordering of the parameters wrong (http://oauth.net/core/1.0/#anchor14)Here's the patch to make the program work:
--- Program.cs 2015-02-23 23:13:03.000000000 -0800
+++ Program3.cs 2015-02-23 23:20:37.000000000 -0800
@@ -15,11 +15,12 @@
String secret = "SECRET";
String requestUrl = "https://bitbucket.org/api/1.0/oauth/request_token";
- string sigBaseStringParams = "oauth_consumer_key=" + key;
+ string sigBaseStringParams = "";
+ sigBaseStringParams += "oauth_callback=http%3A%2F%2Flocal%3Fdump";
+ sigBaseStringParams += "&" + "oauth_consumer_key=" + key;
sigBaseStringParams += "&" + "oauth_nonce=" + GetNonce();
sigBaseStringParams += "&" + "oauth_signature_method=" + "HMAC-SHA1";
sigBaseStringParams += "&" + "oauth_timestamp=" + GetTimeStamp();
- sigBaseStringParams += "&" + "oauth_callback=http%3A%2F%2Flocal%3Fdump";
sigBaseStringParams += "&" + "oauth_version=1.0";
string sigBaseString = "POST&";
sigBaseString += Uri.EscapeDataString(requestUrl) + "&" + Uri.EscapeDataString(sigBaseStringParams);
@@ -44,6 +45,7 @@
public static String PostData(string url, string postData)
{
WebClient w = new WebClient();
+ w.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
return w.UploadString(url, postData);
}