C# Web API method returns 403 Forbidden

6.4k views Asked by At

Solved!!! - See last edit.

In my MVC app I make calls out to a Web API service with HMAC Authentication Filterign. My Get (GetMultipleItemsRequest) works, but my Post does not. If I turn off HMAC authentication filtering all of them work. I'm not sure why the POSTS do not work, but the GETs do.

I make the GET call from my code like this (this one works):

var productsClient = new RestClient<Role>(System.Configuration.ConfigurationManager.AppSettings["WebApiUrl"],
              "xxxxxxxxxxxxxxx", true);

var getManyResult = productsClient.GetMultipleItemsRequest("api/Role").Result;

I make the POST call from my code like this (this one only works when I turn off HMAC):

private RestClient<Profile> profileClient = new RestClient<Profile>(System.Configuration.ConfigurationManager.AppSettings["WebApiUrl"],
        "xxxxxxxxxxxxxxx", true);

[HttpPost]
public ActionResult ProfileImport(IEnumerable<HttpPostedFileBase> files)
{
    //...
    var postResult = profileClient.PostRequest("api/Profile", newProfile).Result;
}

My RestClient builds like this:

public class RestClient<T> where T : class
{
   //...

   private void SetupClient(HttpClient client, string methodName, string apiUrl, T content = null)
    {
        const string secretTokenName = "SecretToken";

        client.BaseAddress = new Uri(_baseAddress);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        if (_hmacSecret)
        {
            client.DefaultRequestHeaders.Date = DateTime.UtcNow;

            var datePart = client.DefaultRequestHeaders.Date.Value.UtcDateTime.ToString(CultureInfo.InvariantCulture);
            var fullUri = _baseAddress + apiUrl;
            var contentMD5 = "";

            if (content != null)
            {
                var json = new JavaScriptSerializer().Serialize(content);
                contentMD5 = Hashing.GetHashMD5OfString(json); // <--- Javascript serialized version is hashed
            }

            var messageRepresentation = 
                methodName + "\n" + 
                contentMD5 + "\n" +
                datePart + "\n" + 
                fullUri;

            var sharedSecretValue = ConfigurationManager.AppSettings[_sharedSecretName];
            var hmac = Hashing.GetHashHMACSHA256OfString(messageRepresentation, sharedSecretValue);

            client.DefaultRequestHeaders.Add(secretTokenName, hmac);
        }
        else if (!string.IsNullOrWhiteSpace(_sharedSecretName))
        {
            var sharedSecretValue = ConfigurationManager.AppSettings[_sharedSecretName];
            client.DefaultRequestHeaders.Add(secretTokenName, sharedSecretValue);
        }
    }

    public async Task<T[]> GetMultipleItemsRequest(string apiUrl)
    {
        T[] result = null;

        try
        {               
            using (var client = new HttpClient())
            {
                SetupClient(client, "GET", apiUrl);

                var response = await client.GetAsync(apiUrl).ConfigureAwait(false);

                response.EnsureSuccessStatusCode();

                await response.Content.ReadAsStringAsync().ContinueWith((Task<string> x) =>
                {
                    if (x.IsFaulted)
                        throw x.Exception;

                    result = JsonConvert.DeserializeObject<T[]>(x.Result);
                });
            }
        }
        catch (HttpRequestException exception)
        {
            if (exception.Message.Contains("401 (Unauthorized)"))
            {

            }
            else if (exception.Message.Contains("403 (Forbidden)"))
            {

            }
        }
        catch (Exception)
        {
        }

        return result;
    }

    public async Task<T> PostRequest(string apiUrl, T postObject)
    {
        T result = null;
        try
        {               
            using (var client = new HttpClient())
            {
                SetupClient(client, "POST", apiUrl, postObject);

                var response = await client.PostAsync(apiUrl, postObject, new JsonMediaTypeFormatter()).ConfigureAwait(false); //<--- not javascript formatted

                response.EnsureSuccessStatusCode();

                await response.Content.ReadAsStringAsync().ContinueWith((Task<string> x) =>
                {
                    if (x.IsFaulted)
                        throw x.Exception;

                    result = JsonConvert.DeserializeObject<T>(x.Result);

                });
            }
        }
        catch (HttpRequestException exception)
        {
            if (exception.Message.Contains("401 (Unauthorized)"))
            {

            }
            else if (exception.Message.Contains("403 (Forbidden)"))
            {

            }
        }
        catch (Exception)
        {
        }

        return result;
    }

   //...

}

My Web API Controller is defined like this:

[SecretAuthenticationFilter(SharedSecretName = "xxxxxxxxxxxxxxx", HmacSecret = true)]      
public class ProfileController : ApiController
{

    [HttpPost]
    [ResponseType(typeof(Profile))]
    public IHttpActionResult PostProfile(Profile Profile)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        GuidValue = Guid.NewGuid(); 

        Resource res = new Resource();
        res.ResourceId = GuidValue;
        var data23 = Resourceservices.Insert(res);

        Profile.ProfileId = data23.ResourceId;
        _profileservices.Insert(Profile);

        return CreatedAtRoute("DefaultApi", new { id = Profile.ProfileId }, Profile);
    }

}

Here is some of what SecretAuthenticationFilter does:

//now try to read the content as string
string content = actionContext.Request.Content.ReadAsStringAsync().Result;
var contentMD5 = content == "" ? "" : Hashing.GetHashMD5OfString(content); //<-- Hashing the non-JavaScriptSerialized
var datePart = "";
var requestDate = DateTime.Now.AddDays(-2);
if (actionContext.Request.Headers.Date != null)
{
    requestDate = actionContext.Request.Headers.Date.Value.UtcDateTime;
    datePart = requestDate.ToString(CultureInfo.InvariantCulture);
}
var methodName = actionContext.Request.Method.Method;
var fullUri = actionContext.Request.RequestUri.ToString();

var messageRepresentation =
    methodName + "\n" +
    contentMD5 + "\n" +
    datePart + "\n" +
    fullUri;

var expectedValue = Hashing.GetHashHMACSHA256OfString(messageRepresentation, sharedSecretValue);

// Are the hmacs the same, and have we received it within +/- 5 mins (sending and
// receiving servers may not have exactly the same time)
if (messageSecretValue == expectedValue
    && requestDate > DateTime.UtcNow.AddMinutes(-5)
    && requestDate < DateTime.UtcNow.AddMinutes(5))
    goodRequest = true;

Any idea why HMAC doesn't work for the POST?

EDIT:
When SecretAuthenticationFilter tries to compare the HMAC sent, with what it thinks the HMAC should be they don't match. The reason is the MD5Hash of the content doesn't match the MD5Hash of the received content. The RestClient hashes the content using a JavaScriptSerializer.Serialized version of the content, but then the PostRequest passes the object as JsonMediaTypeFormatted.

These two types don't get formatted the same. For instance, the JavaScriptSerializer give's us dates like this: \"EnteredDate\":\"\/Date(1434642998639)\/\"

The passed content has dates like this: \"EnteredDate\":\"2015-06-18T11:56:38.6390407-04:00\"

I guess I need the hash to use the same data that's passed, so the Filter on the other end can confirm it correctly. Thoughts?

EDIT: Found the answer, I needed to change the SetupClient code from using this line:

var json = new JavaScriptSerializer().Serialize(content);
contentMD5 = Hashing.GetHashMD5OfString(json);

To using this:

var json = JsonConvert.SerializeObject(content);
contentMD5 = Hashing.GetHashMD5OfString(json);

Now the sent content (formatted via JSON) will match the hashed content.

I was not the person who wrote this code originally. :)

1

There are 1 answers

0
M Kenyon II On BEST ANSWER

Found the answer, I needed to change the SetupClient code from using this line:

var json = new JavaScriptSerializer().Serialize(content);
contentMD5 = Hashing.GetHashMD5OfString(json);

To using this:

var json = JsonConvert.SerializeObject(content);
contentMD5 = Hashing.GetHashMD5OfString(json);

Now the content used for the hash will be formatted as JSON and will match the sent content (which is also formatted via JSON).