为什么 BitBucket OAuth request_token API 总是 return 400 "Could not verify OAuth request."?

Why does BitBucket OAuth request_token API always return 400 "Could not verify OAuth request."?

为了让 BitBucket OAuth API 正常工作,我已经用头撞了整整一个星期,但我什至无法获得一个请求令牌来挽救自己的生命。我在这里制作的代码适用于 LinkedIn,但 BitBucket 始终 returns HTTP 状态代码 400 以及非常详细且有用的消息:"Could not verify OAuth request"。

这是一个简单的代码(50 行,不能再短了),就是这样,因为我需要手动实现 OAuth,否则我不会在这里询问并使用其他外部库,但由于公司的要求我不允许在这个项目中使用外部库。不过,我看不出有什么问题,1.0a 并不难,获取请求令牌不应该花这么长时间。有什么问题吗?

我也检查了我的时间戳,它很好,w32tm.exe 对比 pool.ntp.org returns 时间 +30 什么的。我也试过在 UtcNow 时间戳中添加和删除 30 分钟但没有成功,但我的时钟已正确同步(使用本地时间和正确的 GMT 值 (GMT -4:30))所以它根本没有意义。

会不会是因为我在公司防火墙 (Forefront) 后面?但是,为什么 LinkedIn 的 API 调用有效而 BitBucket 却不行?我还阅读了 很多 文档,例如 OAuth 圣经、RFC、官方文档等。当然,在询问之前对 SO 进行了广泛的搜索并查看了 所有点击"Post your question"按钮前"Similar Questions"面板中显示的链接。

这是简单的代码 (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)));
        }

    }
}

编辑: 这是按要求捕获的 fiddler 和浏览器:

提前致谢!

编辑 2: 我做了一个新的测试,使用了 n0741337 的建议提供的 OAuthBase.cs class,但是 class 确实不符合要求回调方法的 1.0A 规范(至少 Bitbucket 足够友好地说需要这样的参数)所以我不得不修改它以便它在基本签名字符串中包含回调参数(在原始没有编码的格式)。虽然结果相同(我认为如果我显示我的密钥并不重要,因为无论如何你都不会看到我的密钥),这是一个捕获:

这是签名基本字符串:

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

此外,为了质疑我的代码,还可以使用 this class 我发现有人已经针对 Twitter 进行了调整(尽管 Twitter 在我的工作场所被屏蔽,所以我无法对其进行测试),但是结果是一样的。

这是我使用 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();
        }

    }
}

编辑 2 的问题: 有谁知道通过这种方法连接到 Bitbucket 服务的应用程序,以便我可以 运行 Fiddler 并查看它发送的内容?如果我能看到一些输出,我至少可以复制流程:/我已经尝试过对 SourceTree 但效果不是很好。

编辑 3: 根据 AZ. 的建议,我用这个更改了时间戳生成代码,但无论如何它都不起作用:(。时间戳值看起来还不错,有我的时间戳与服务器的时间戳仅相差 5 秒:

TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
String timeStamp = Convert.ToInt64(ts.TotalSeconds).ToString();

此外,我注意到签名包含一个“+”,它应该被编码为 %20,我在编辑问题后注意到这一点时就这样做了,但它也不起作用,仅供参考。

不确定是否是这个原因,但这行很臭:

TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);

您正在从 UTC 日期中减去 DateTimeKind.Unspecified 日期。我没有可用于测试这个的环境,但构建 1970 年的日期作为 UTC 可能会有所帮助

我怀疑你的问题是你正在本地开发服务器上调试它,而你发送的这个 URL 作为回调 URL 对互联网上的客户端是无用的:

GETArgs += "&oauth_callback=" + a.UrlEncode("http://localhost:4000/");

我还没有阅读 BitBucket 的文档,但是 Google OAuth 的实现方式是您需要有一个可公开访问的 URL 以便将回调消息发送到为了让握手工作。

此 URL 不会被您的浏览器回调,它是从 他们的服务器 调用的,因此它必须可以访问 Internet。

你的程序有两处错误:

  • 你没有发送 Content-Type: application/x-www-form-urlencoded header,这使得 POST body 无法理解(通常建议使用授权请求 header ).
  • 您没有正确创建签名基本字符串。签名基础字符串是 OAuth 1.0 中最微妙的部分,因为客户端和服务器必须独立地产生完全相同的字节序列。使用 oauth_callback 你得到了错误的参数顺序 (http://oauth.net/core/1.0/#anchor14)

这是使程序运行的补丁:

--- 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);
         }