GET не работает с 401 (неавторизовано), когда задействован параметр запроса из-за недопустимой подписи OAuth

Я запрашиваю API, использующий OAuth1. Я могу успешно выполнить запрос с помощью RestSharp:

var client = new RestClient("https://api.thirdparty.com/1/");
client.Authenticator = 
    OAuth1Authenticator.ForAccessToken(appKey, appSecret, token, tokenSecret);

var request = new RestRequest("projects/12345/documents", Method.GET);
request.AddParameter("recursive", "true");

var response = client.Execute(request);

К сожалению, я не могу использовать RestSharp в реальном проекте (я просто использую его здесь, чтобы убедиться, что вызов API работает), поэтому я попытался заставить OAuth работать, используя простой .NET.

У меня он работает с запросами, которые не используют параметры запроса, поэтому простой переход с GET на https://api.thirdparty.com/1/projects/12345/documents отлично работает.

Как только я пытаюсь использовать параметр запроса, например https://api.../documents?recursive=true, как показано в примере RestSharp, я получаю сообщение об ошибке 401 Unauthorized, поскольку считаю, что моя подпись OAuth недействительна.

Вот как я генерирую подпись и запрос OAuth. Может ли кто-нибудь сказать мне, как сгенерировать действительную подпись, когда задействованы параметры запроса?

static string appKey = @"e8899de00";
static string appSecret = @"bffe04d6";
static string token = @"6e85a21a";
static string tokenSecret = @"e137269f";
static string baseUrl = "https://api.thirdparty.com/1/";
static HttpClient httpclient;
static HMACSHA1 hasher;
static DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

static void Main(string[] args)
{
    // SETUP HTTPCLIENT
    httpclient = new HttpClient();
    httpclient.BaseAddress = new Uri(baseUrl);
    httpclient.DefaultRequestHeaders.Accept.Clear();
    httpclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // SETUP THE HASH MESSAGE AUTHENTICATION CODE (HMAC) WITH SHA1
    hasher = new HMACSHA1(new ASCIIEncoding().GetBytes(string.Format("{0}&{1}", appSecret, tokenSecret)));

    // WORKS IF QUERY PARAMETER IS MISSED OFF THE END, FAILS WITH 401 IF INCLUDED
    var document = 
        Request(HttpMethod.Get, "projects/12345/documents?recursive=true");
}

static string Request(HttpMethod method, string url)
{
    // CREATE A TIMESTAMP OF CURRENT EPOCH TIME
    var timestamp = (int)((DateTime.UtcNow - epoch).TotalSeconds);

    // DICTIONARY WILL HOLD THE KEY/PAIR VALUES FOR OAUTH
    var oauth = new Dictionary<string, string>();
    oauth.Add("oauth_consumer_key", appKey);
    oauth.Add("oauth_signature_method", "HMAC-SHA1");
    oauth.Add("oauth_timestamp", timestamp.ToString());
    oauth.Add("oauth_nonce", "nonce");
    oauth.Add("oauth_token", token);
    oauth.Add("oauth_version", "1.0");

    // GENERATE OAUTH SIGNATURE
    oauth.Add("oauth_signature", GenerateSignature(method.ToString(), string.Concat(baseUrl, url), oauth));

    // GENERATE THE REQUEST
    using (var request = new HttpRequestMessage())
    {
        // URL AND METHOD
        request.RequestUri = new Uri(string.Concat(baseUrl, url));
        request.Method = method;

        // GENERATE AUTHORIZATION FOR THIS REQUEST
        request.Headers.Add("Authorization", GenerateOAuthHeader(oauth));

        // MAKE REQUEST
        var response = httpclient.SendAsync(request).Result;

        // ENSURE IT WORKED
        response.EnsureSuccessStatusCode(); // THROWS 401 UNAUTHORIZED

        // RETURN CONTENT
        return response.Content.ReadAsStringAsync().Result;
    };
}

static string GenerateSignature(string verb, string url, Dictionary<string, string> data)
{
    var signaturestring = string.Join(
        "&",
        data
            .OrderBy(s => s.Key)
            .Select(kvp => string.Format(
                    "{0}={1}",
                    Uri.EscapeDataString(kvp.Key), 
                    Uri.EscapeDataString(kvp.Value))
                    )
    );

    var signaturedata = string.Format(
        "{0}&{1}&{2}",
        verb,
        Uri.EscapeDataString(url),
        Uri.EscapeDataString(signaturestring.ToString())
    );

    return Convert.ToBase64String(hasher.ComputeHash(new ASCIIEncoding().GetBytes(signaturedata.ToString())));
}

static string GenerateOAuthHeader(Dictionary<string, string> data)
{
    return "OAuth " + string.Join(
        ", ",
        data
            .Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
            .OrderBy(s => s)
    );
}

person Equalsk    schedule 05.04.2018    source источник
comment
Но вы не включаете в подпись параметры получения из строки запроса. В вашем примере вам нужно добавить рекурсию в список пар ключ-значение, которые вы подписываете.   -  person Evk    schedule 05.04.2018
comment
(И исключить строку запроса из URL-адреса при подписании).   -  person Evk    schedule 05.04.2018
comment
@Evk Да, похоже, именно так. Мне пришлось включить все пары ключей OAuth, а также все параметры запроса в качестве пар ключей при подписании, но включить только ключи OAuth в заголовок. Вы хотите написать ответ? Если нет, то сам поставлю образец.   -  person Equalsk    schedule 06.04.2018


Ответы (1)


В oauth 1 параметры строки запроса (а также параметры POST в случае POST с x-www-form-urlencoded, поэтому они похожи на «a = 1 & b = 2» в теле) должны быть включены в список пар ключ-значение, которые вы затем сортируете и подписываете. Итак, чтобы получить правильную подпись, вам необходимо:

  • извлекать все параметры строки запроса (и POST, как указано выше) в виде пар ключ-значение
  • удалить строку запроса из url
  • подпишите все, что вы делаете сейчас (URL-адрес без строки запроса и все пары ключей, включая извлеченные выше и специфичные для oauth)
person Evk    schedule 06.04.2018