Skip to main content

An OAuth 1.0a signature validation helper for .NET Core

Even though the OAuth 1.0a specification has been officially made obsolete by OAuth 2.0, it remains in active use for legacy implementations. A recent project required me to be able to consume an OAuth 1.0a request, specifically to calculate and validate the oauth_signature parameter. This was in a .NET Core 3.0 project, which (as far as I can tell!) doesn't have any libraries that help with this specification.

I really didn't feel comfortable taking a dependency hit on this one by bringing in a third-party NuGet package. Many of the options provided far more than I needed, and most were (understandably) no longer in active development or supported. Taking on an unsupported "black box" dependency with anything related to security doesn't sit quite right with me.

So I rolled my own implementation for just the subset of features that I needed to support (verification only, and OAuth parameters passed as form post data). This is not meant to be a complete implementation, but can serve as a starting point for anyone else who finds themselves in a similar situation and isn't interested in bringing in a dependency.

The latest code is on 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;
    }
}

Comments

Popular posts from this blog

Integrating Google reCAPTCHA v2 with an ASP.NET Core Razor Pages form

CAPTCHA ( c ompletely a utomated p ublic T uring test to tell c omputers and h umans a part) reduces the likelihood of automated bots successfully submitting forms on your web site. Google's reCAPTCHA implementation is familiar to users (as these things go), and is a free service. Integrating it within an ASP.NET Core Razor Pages form is straightforward. Obtain Google API key The first thing we need to do is to sign up for an API key pair. The latest documentation for this process can be found on the reCAPTCHA Developer's Guide . Google will provide us with a site key and a secret key . I am storing these keys in the Visual Studio Secret Manager so they are not accidentally checked in with version control. To access the Secret Manager, right-click on the project and select "Manage User Secrets...": No, these are not my real keys... As part of the sign up process for the reCAPTCHA keys, we specify the domains for which the keys are valid. Be sure to add &qu

Mitigating the risk of brute force login compromise using Redis cache in ASP.NET Core Identity

Any application that requires user authentication must take adequate steps to protect the user accounts for which it is responsible. This includes correctly handling workflows such as proper password hashing and storage, providing feedback that doesn't disclose information useful to an attacker, providing means for password reset, etc. The ASP.NET Core Identity membership system provides much of this functionality out-of-the-box, using tried and tested implementations that avoid common mistakes and pitfalls. It is an excellent platform on which to build when developing your application's authentication system. ASP.NET Core Identity provides a means of mitigating brute force login attempts through user lockout. After a configurable number of failed login attempts, a user's account is locked for a period of time. Both the maximum number of attempts, and the lockout period, are configurable. While this is certainly a valid strategy, it does have some weaknesses: The syst

Detecting the user's time zone at registration

This article walks through the process of capturing and detecting a user's time zone at the point of registration. This information can then be used to display time-related information in the local zone of the user. Note that, in most cases, you should still store datetime information in UTC; the time zone is only used for local display. This setup assumes a site running Razor Pages in ASP.NET Core 2.2, although there's nothing particularly framework-specific in what follows. The same ideas should carry over quite easily to, for example, a regular ASP.NET MVC site. We start with a basic registration page: Register.cshtml @page @model RegisterModel <h1> Create an account </h1> <form method= "post" > <div asp-validation-summary= "ModelOnly" ></div> <div class= "form-group" > <label asp-for= "FirstName" ></label> <input asp-for= "FirstName"