ASP NET Core Twitter OAuth 请求令牌问题

ASP NET Core Twitter OAuth Request Token Issues

背景

我有一个设置了 Twitter 应用程序的后端应用程序,我可以查询和提取用户 tweet/post 数据。这很好,但是,现在在前端我没有完整的 Twitter 集成设置。我的意思是,在前端,用户可以输入任何 Twitter 用户名,我想确定输入的 Twitter 用户名确实属于用户。使用 Twitter 应用程序密钥,您可以为任何 Twitter 帐户提取 public Twitter 数据,这适用于大规模数据摄取,在我的案例中是概念验证类工作。就我现在而言,我需要在后端强制执行假设,即针对特定 Twitter 屏幕名称分析的数据也归我的 Web 应用程序上的帐户用户所有。

建议的 Twitter 解决方案

这是我一直在努力遵循的一堆参考文档。

https://developer.twitter.com/en/docs/basics/authentication/guides/log-in-with-twitter https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token https://oauth.net/core/1.0/#anchor9 https://oauth.net/core/1.0/#auth_step1

我一直在尝试遵循这一点,并且我对下面发布的代码有不同的排列(一个没有回调 URL 作为参数,一个有等等)但在这一点上,没有太大的不同。我没有任何成功,而且已经超过几天了,这让我很难受。

代码

这是我尝试遵循上面文档中提出的 OAuth 规范。请注意,这是 ASP.NET Core 2.2 + 代码。此外,这是 Twitter 指南中用于 OAuth 身份验证和授权的步骤 1 的代码。

public async Task<string> GetUserOAuthRequestToken()
{
    int timestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
    string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(timestamp.ToString()));

    string consumerKey = twitterConfiguration.ConsumerKey;
    string oAuthCallback = twitterConfiguration.OAuthCallback;

    string requestString =
        twitterConfiguration.EndpointUrl +
        OAuthRequestTokenRoute;

    string parameterString =
        $"oauth_callback={WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}&" +
        $"oauth_consumer_key={twitterConfiguration.ConsumerKey}&" +
        $"oauth_nonce={nonce}&" +
        $"oauth_signature_method=HMAC_SHA1&" +
        $"oauth_timestamp={timestamp}" +
        $"oauth_version=1.0";

    string signatureBaseString =
        "POST&" +
        WebUtility.UrlEncode(requestString) +
        "&" +
        WebUtility.UrlEncode(parameterString);

    string signingKey =
        twitterConfiguration.ConsumerSecret +
        "&" + twitterConfiguration.AccessTokenSecret;

    byte[] signatureBaseStringBytes = Encoding.ASCII.GetBytes(signatureBaseString);
    byte[] signingKeyBytes = Encoding.ASCII.GetBytes(signingKey);

    HMACSHA1 hmacSha1 = new HMACSHA1(signingKeyBytes);
    byte[] signature = hmacSha1.ComputeHash(signatureBaseStringBytes);

    string authenticationHeaderValue =
        $"oauth_nonce=\"{nonce}\", " +
        $"oauth_callback=\"{WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}\", " +
        $"oauth_signature_method=\"HMAC_SHA1\", " +
        $"oauth_timestamp=\"{timestamp}\", " +
        $"oauth_consumer_key=\"{twitterConfiguration.ConsumerKey}\", " +
        $"oauth_signature=\"{Convert.ToBase64String(signature)}\", " +
        $"oauth_version=\"1.0\"";

    HttpRequestMessage request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri(
        baseUri: new Uri(twitterConfiguration.EndpointUrl),
        relativeUri: OAuthRequestTokenRoute);
    request.Content = new FormUrlEncodedContent(
        new Dictionary<string, string>() {
            { "oauth_callback", twitterConfiguration.OAuthCallback }
        });
    request.Headers.Authorization = new AuthenticationHeaderValue("OAuth",
        authenticationHeaderValue);

    HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);

    if (httpResponseMessage.IsSuccessStatusCode)
    {
        return await httpResponseMessage.Content.ReadAsStringAsync();
    }
    else
    {
        return null;
    }
}

备注 我也尝试从参数中删除回调 URL,但没有用。我已经尝试了各种略有不同的排列(对我的签名进行 urlencoded,在查询字符串中添加回调 URL,将其删除等),但此时我已经忘记了我已经尝试过但还没有尝试过的(编码、引号等)。

请忽略我尚未将响应序列化为模型的事实,因为目标是首先获得成功状态代码!

我也有针对此方法的集成测试设置,但我不断收到 400 Bad Request 且没有其他信息(这很有意义),但绝对无助于调试。

[Fact]
public async Task TwitterHttpClientTests_GetOAuthRequestToken_GetsToken()
{
    var result = await twitterHttpClient.GetUserOAuthRequestToken();

    Assert.NotNull(result);
}

顺便说一句,我还有其他一些问题:

  1. 有没有办法不用去验证用户的 Twitter 帐户 通过 OAuth 流程?我问这个的原因是因为得到 通过 OAuth 流程被证明是困难的
  2. 在后端做 Twitter 登录工作流程的第一步和 return 对前端的响应是否安全?响应 将携带敏感令牌和令牌秘密。 (如果我要回答 这是我自己,我会说你必须这样做,否则你 必须将应用程序机密硬编码到前端配置中 哪个更糟)。我问这个是因为这是我的意识 因为我已经开始了,我有点担心。
  3. 是否有用于 C# ASP.NET 核心的 OAuth 帮助程序库可以使这更容易?

我通过编写单元测试和研究 Creating A Signature 上的 Twitter 文档解决了这个问题。由于该示例提供了密钥和结果,因此可以验证您的代码是否正确。

既然你问了图书馆 - 我写了 LINQ to Twitter 希望能帮助像我这样的人完成这项艰巨的任务。

除了签名之外,页面导航可能具有挑战性,因为您的代码通过 OAuth 流工作。请查看 Obtaining user access tokens to understand this better. I've also documented this in the LINQ to Twitter Wiki on Securing your Applications 上的 Twitter 文档。以下是这将如何与 LINQ to Twitter 一起使用:

  1. 首先,我有一个 OAuthController 和一个 Begin 操作来将用户重定向到以启动身份验证过程:
public async Task<ActionResult> Begin()
{
    //var auth = new MvcSignInAuthorizer
    var auth = new MvcAuthorizer
    {
        CredentialStore = new SessionStateCredentialStore(HttpContext.Session)
        {
            ConsumerKey = configuration["Twitter:ConsumerKey"],
            ConsumerSecret = configuration["Twitter:ConsumerSecret"]
        }
    };

    string twitterCallbackUrl = Request.GetDisplayUrl().Replace("Begin", "Complete");
    return await auth.BeginAuthorizationAsync(new Uri(twitterCallbackUrl));
}

请注意,它使用 MvcSignInAuthorizer,通过 CredentialStore 属性 传递凭据。如果您使用自己的原始代码,您将使用 Authorization header.

设置 HTTP 请求

接下来,请注意我正在修改当前的 URL,以便它将引用相同的控制器,但具有 Complete 端点。那就是发送到 Twitter 授权的 oauth_callback

  1. 该过程将用户重定向到 Twitter 网站,他们授权您的应用程序,然后它使用 oauth_callback 将用户重定向回您的网站。以下是您的处理方式:
public async Task<ActionResult> Complete()
{
    var auth = new MvcAuthorizer
    {
        CredentialStore = new SessionStateCredentialStore(HttpContext.Session)
    };

    await auth.CompleteAuthorizeAsync(new Uri(Request.GetDisplayUrl()));

    // This is how you access credentials after authorization.
    // The oauthToken and oauthTokenSecret do not expire.
    // You can use the userID to associate the credentials with the user.
    // You can save credentials any way you want - database, 
    //   isolated storage, etc. - it's up to you.
    // You can retrieve and load all 4 credentials on subsequent 
    //   queries to avoid the need to re-authorize.
    // When you've loaded all 4 credentials, LINQ to Twitter will let 
    //   you make queries without re-authorizing.
    //
    //var credentials = auth.CredentialStore;
    //string oauthToken = credentials.OAuthToken;
    //string oauthTokenSecret = credentials.OAuthTokenSecret;
    //string screenName = credentials.ScreenName;
    //ulong userID = credentials.UserID;
    //

    return RedirectToAction("Index", "Home");
}

同样,您可以看到我正在使用 MvcAuthorizer 并完成请求。完成请求后,您将能够提取 oauth_tokenoauth_token_secret,以及 screen_nameuser_id。您可以保存这些工件并 re-use 它们供该用户所有后续 activity 使用,从而改善他们的体验,因为他们不必在您每次需要发出请求时都登录。

关于您关于验证的问题,有一个 Verify Credentials 端点。

如果您想了解更多信息,LINQ to Twitter 有一个 ASP.NET Core Sample, API Samples with 100% API coverate, and full documentation on the Wiki

经过数小时的查阅文档后,我找到了答案。原来我错过了指南中的一些小细节。

  1. oauth/request_token 提出请求时,当您签署 请求,您不使用访问令牌秘密(对于此特定请求)。另外,请参阅签署请求指南的 "Getting Signing Key" 部分并阅读最后几段。因此签名密钥 没有访问令牌秘密
  2. 您必须对每个键和值进行 UrlEncode。您还必须对授权进行 UrlEncode header。

我会 post 在这里为大家更新代码,以备您在 C# 中需要。请注意,此代码不干净。您应该将 OAuth 功能分离到其他一些 class。这是我试图让它发挥作用的尝试。

public async Task<string> GetUserOAuthRequestToken()
{
    int timestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
    string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(timestamp.ToString()));

    string consumerKey = twitterConfiguration.ConsumerKey;
    string oAuthCallback = twitterConfiguration.OAuthCallback;

    string requestString =
        twitterConfiguration.EndpointUrl +
        OAuthRequestTokenRoute;

    string parameterString =
        $"oauth_callback={WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}&" +
        $"oauth_consumer_key={WebUtility.UrlEncode(twitterConfiguration.ConsumerKey)}&" +
        $"oauth_nonce={WebUtility.UrlEncode(nonce)}&" +
        $"oauth_signature_method={WebUtility.UrlEncode(OAuthSigningAlgorithm)}&" +
        $"oauth_timestamp={WebUtility.UrlEncode(timestamp.ToString())}&" +
        $"oauth_version={WebUtility.UrlEncode("1.0")}";

    string signatureBaseString =
        "POST&" +
        WebUtility.UrlEncode(requestString) +
        "&" +
        WebUtility.UrlEncode(parameterString);

    string signingKey =
        WebUtility.UrlEncode(twitterConfiguration.ConsumerSecret) +
        "&";

    byte[] signatureBaseStringBytes = Encoding.ASCII.GetBytes(signatureBaseString);
    byte[] signingKeyBytes = Encoding.ASCII.GetBytes(signingKey);

    HMACSHA1 hmacSha1 = new HMACSHA1(signingKeyBytes);
    byte[] signature = hmacSha1.ComputeHash(signatureBaseStringBytes);
    string base64Signature = Convert.ToBase64String(signature);

    string authenticationHeaderValue =
        $"oauth_nonce=\"{WebUtility.UrlEncode(nonce)}\", " +
        $"oauth_callback=\"{WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}\", " +
        $"oauth_signature_method=\"{WebUtility.UrlEncode(OAuthSigningAlgorithm)}\", " +
        $"oauth_timestamp=\"{WebUtility.UrlEncode(timestamp.ToString())}\", " +
        $"oauth_consumer_key=\"{WebUtility.UrlEncode(twitterConfiguration.ConsumerKey)}\", " +
        $"oauth_signature=\"{WebUtility.UrlEncode(base64Signature)}\", " +
        $"oauth_version=\"{WebUtility.UrlEncode("1.0")}\"";

    HttpRequestMessage request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri(
        baseUri: new Uri(twitterConfiguration.EndpointUrl),
        relativeUri: OAuthRequestTokenRoute);
    request.Headers.Authorization = new AuthenticationHeaderValue("OAuth",
        authenticationHeaderValue);

    HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);

    if (httpResponseMessage.IsSuccessStatusCode)
    {
        string response = await httpResponseMessage.Content.ReadAsStringAsync();
        return response;
    }
    else
    {
        return null;
    }
}