Библиотека для проверки подписи OAuth 1.0a в ASP.NET Core

Мне нужно проверить запросы OAuth 1.0a (RFC 5849) на сайте ASP.NET Core. Обновление клиента до OAuth 2.0 или чего-то еще не вариант. Я понимаю спецификацию, но реализация процесса проверки для oauth_signature кажется немного хрупкой, и, конечно же, здесь нет необходимости изобретать велосипед.

Есть ли в .NET Core какие-либо встроенные классы для обработки этого? В идеале, что-то, где вы просто передаете HttpRequest и секретный ключ, и он говорит вам, действительна ли подпись?

Если нет ничего встроенного, какие-либо рекомендации по сторонним библиотекам, которые могли бы справиться с этим для меня?


person Dave Mateer    schedule 08.11.2019    source источник


Ответы (1)


Я действительно не чувствовал себя комфортно, получая удар по этой зависимости, добавляя сторонний пакет NuGet. Многие из опций предоставляли гораздо больше, чем мне было нужно, и большинство из них (по понятным причинам) больше не находились в активной разработке или не поддерживались. Принятие неподдерживаемой зависимости «черного ящика» от чего-либо, связанного с безопасностью, мне не совсем подходит.

Поэтому я реализовал свою собственную реализацию только для подмножества функций, которые мне нужно было поддерживать (только проверка и параметры OAuth, передаваемые как данные формы). Это не полная реализация, но она может послужить отправной точкой для всех, кто оказался в похожей ситуации и не заинтересован во введении зависимости.

Последний код находится на utility_netcore" rel="nofollow noreferrer">GitHub.

public static class OAuth1Utilities
{
    private static readonly Lazy<bool[]> UnreservedCharacterMask = new Lazy<bool[]>(CreateUnreservedCharacterMask);

    public static string EncodeString(string value)
    {
        byte[] characterBytes = Encoding.UTF8.GetBytes(value);

        StringBuilder encoded = new StringBuilder();
        foreach (byte character in characterBytes)
        {
            if (UnreservedCharacterMask.Value[character])
            {
                encoded.Append((char)character);
            }
            else
            {
                encoded.Append($"%{character:X2}");
            }
        }

        return encoded.ToString();
    }

    public static string GetBaseStringUri(HttpRequest request)
    {
        StringBuilder baseStringUri = new StringBuilder();
        baseStringUri.Append(request.Scheme.ToLowerInvariant());
        baseStringUri.Append("://");
        baseStringUri.Append(request.Host.ToString().ToLowerInvariant());
        baseStringUri.Append(request.Path.ToString().ToLowerInvariant());
        return baseStringUri.ToString();
    }

    public static string GetNormalizedParameterString(HttpRequest request)
    {
        var parameters = new List<(string key, string value)>();

        foreach (var queryItem in request.Query)
        {
            foreach (var queryValue in queryItem.Value)
            {
                parameters.Add((queryItem.Key, queryValue));
            }
        }

        foreach (var formItem in request.Form)
        {
            foreach (var formValue in formItem.Value)
            {
                parameters.Add((formItem.Key, formValue));
            }
        }

        parameters.RemoveAll(_ => _.key == "oauth_signature");

        parameters = parameters
            .Select(_ => (key: EncodeString(_.key), value: EncodeString(_.value)))
            .OrderBy(_ => _.key)
            .ThenBy(_ => _.value).ToList();

        return string.Join("&", parameters.Select(_ => $"{_.key}={_.value}"));
    }

    public static string GetSignature(HttpRequest request, string clientSharedSecret, string tokenSharedSecret)
    {
        string signatureBaseString = GetSignatureBaseString(request);
        return GetSignature(signatureBaseString, clientSharedSecret, tokenSharedSecret);
    }

    public static string GetSignature(string signatureBaseString, string clientSharedSecret, string tokenSharedSecret)
    {
        string key = $"{EncodeString(clientSharedSecret)}&{EncodeString(tokenSharedSecret)}";
        var signatureAlgorithm = new HMACSHA1(Encoding.ASCII.GetBytes(key));
        byte[] digest = signatureAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString));
        return Convert.ToBase64String(digest);
    }

    public static string GetSignatureBaseString(HttpRequest request)
    {
        StringBuilder signatureBaseString = new StringBuilder();
        signatureBaseString.Append(request.Method.ToUpperInvariant());
        signatureBaseString.Append("&");
        signatureBaseString.Append(EncodeString(GetBaseStringUri(request)));
        signatureBaseString.Append("&");
        signatureBaseString.Append(EncodeString(GetNormalizedParameterString(request)));
        return signatureBaseString.ToString();
    }

    public static bool VerifySignature(HttpRequest request, string clientSharedSecret, string tokenSharedSecret)
    {
        string actualSignature = request.Form["oauth_signature"];
        string expectedSignature = GetSignature(request, clientSharedSecret, tokenSharedSecret);
        return expectedSignature == actualSignature;
    }

    private static bool[] CreateUnreservedCharacterMask()
    {
        bool[] mask = new bool[byte.MaxValue];

        // hyphen
        mask[45] = true;

        // period
        mask[46] = true;

        // 0-9
        for (int pos = 48; pos <= 57; pos++)
        {
            mask[pos] = true;
        }

        // A-Z
        for (int pos = 65; pos <= 90; pos++)
        {
            mask[pos] = true;
        }

        // underscore
        mask[95] = true;

        // a-z
        for (int pos = 97; pos <= 122; pos++)
        {
            mask[pos] = true;
        }

        // tilde
        mask[126] = true;

        return mask;
    }
}
person Dave Mateer    schedule 15.11.2019