1

I am currently working on an app using Xamarin Forms. My development environment is Mac OS, Visual Studio, and C#. The app that I am developing will be interfacing with a web service. The service is FamilySearch, which is a genealogy website.

I have been writing some code to send requests and handle responses from their servers. I wrote a request that I believed was well-formed, but I received a response indicating "Unauthorized".

I then decided to take my code run it in a .NET Core console application, so as to avoid the overhead of using Xamarin Forms. I sent the same exact request, byte for byte. When doing this, I get a successful response (status code 200).

So, I have 2 identical HTTP requests, one being sent from the iOS simulator in a Xamarin Forms app, and the other being sent from the console in a .NET Core app. They are the receiving different responses from the server. Any idea why this could be?

Here is some code so you have an idea of what I am doing. First, I set up some HttpClient objects (one that is directed at their authentication server, another at a server the handles other calls):

HttpClient _identity_host = new HttpClient();
HttpClient _platform_host = new HttpClient();
_identity_host.BaseAddress = new System.Uri("https://identint.familysearch.org");
_platform_host.BaseAddress = new System.Uri("https://api-integ.familysearch.org");
_identity_host.DefaultRequestHeaders.Accept.Add(
    new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
_platform_host.DefaultRequestHeaders.Accept.Add(
    new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

After setting up the HttpClient objects, I then call into this net function to log in to my user account. This function is successful in both the Xamarin Forms app and the .NET Core console app:

public async void AttemptLogin(string username, string password)
{
    //Form the web request
    Dictionary<string, string> login_content_pairs = new Dictionary<string, string>()
    {
        { "password", _password },
        { "grant_type", "password" },
        { "client_id", _application_id },
        { "username", _username }
    };

    string login_content = this.ToQueryString(login_content_pairs, false);
    StringContent content = new StringContent(login_content, Encoding.UTF8, "application/x-www-form-urlencoded");
    var result = await _identity_host.PostAsync("/cis-web/oauth2/v3/token", content);
    if (result.IsSuccessStatusCode)
    {
        var token_json = await result.Content.ReadAsStringAsync();
        JObject parsed_json = JObject.Parse(token_json);

        if (parsed_json.ContainsKey("access_token"))
        {
            _access_token = (string)parsed_json["access_token"];
        }
        else if (parsed_json.ContainsKey("token"))
        {
            _access_token = (string)parsed_json["token"];
        }

        string k = (string)parsed_json["token"];

        //Set the authorization header on the platform host object
        _platform_host.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _access_token);
        _successful_login = true;
    }
}

Finally, after the login attempt completes, I then use my authorization token to request some stuff from their servers:

public async void GetCurrentPerson()
{
    var result = await _platform_host.GetAsync("/platform/tree/current-person");
    if (result.IsSuccessStatusCode)
    {
        var token_json = await result.Content.ReadAsStringAsync();
        JObject parsed_json = JObject.Parse(token_json);
    }
}

The above GET request is the one that is returning two different responses - depending on whether I am using the iOS simulator with Xamarin Forms or using a .NET Core console app.

Here is the ToString() of the GET request from the Visual Studio debugger:

{
    Method: GET,
    RequestUri: 'https://api-integ.familysearch.org/platform/tree/persons/L5FY-BQW',
    Version: 1.1,
    Content: <null>,
    Headers:
    {   
        Accept: application/json
        Authorization: Bearer MY_AUTHORIZATION_TOKEN
    }
}

From the Console app, I get this response:

{
    StatusCode: 200,
    ReasonPhrase: 'OK',
    Version: 1.1,
    Content: System.Net.Http.NoWriteNoSeekStreamContent,
    Headers:
    {
        Cache-Control: no-transform, must-revalidate, max-age=0
        Date: Wed, 04 Apr 2018 01:40:13 GMT
        ETag: "137412002955880000"
        Server: Apache-Coyote/1.1
        Vary: Accept
        Vary: Accept-Language
        Vary: Accept-Encoding
        Vary: Expect
        Vary: Accept-Encoding
        Warning: 199 FamilySearch Best Practice Violation: Should specify versioned media type in Accept header, e.g. one of [ "application/x-fs-v1+xml", "application/x-fs-v1+json", "application/atom+xml", "application/x-gedcomx-atom+json", "application/x-gedcomx-v1+xml", "application/x-gedcomx-v1+json" ].
        X-PROCESSING-TIME: 184
        Connection: keep-alive
        Allow: OPTIONS
        Allow: HEAD
        Allow: GET
        Allow: POST
        Allow: DELETE
        Content-Location: /tree/persons/L5FY-BQW
        Content-Type: application/json
        Last-Modified: Sat, 24 Mar 2018 16:04:55 GMT
        Content-Length: 6479
    }
}

While the same request from the Xamarin Forms app using the iOS simulator yields the following response:

{
    StatusCode: 401, 
    ReasonPhrase: 'Unauthorized', 
    Version: 1.1, 
    Content: System.Net.Http.StreamContent, 
    Headers: 
    { 
        Cache-Control: no-cache, no-store, no-transform, must-revalidate, max-age=0 
        Date: Wed, 04 Apr 2018 01:45:02 GMT 
        Link: <https://integration.famil...
    }
}

The content of the 401 Unauthorized response is the following:

{
    "errors" : [ {
        "code" : 401,
        "message" : "Unable to  read tf person.",
        "stacktrace" : "GET http://tf.integ.us-east-1.dev.fslocal.org/tf/person/L5FY-BQW?oneHops=none returned a response status of 401 Unauthorized: { "401" : "Unauthorized" }"
    } ]
}

Any help in understanding what is going wrong would be greatly appreciated. Thank you!

David
  • 1,847
  • 4
  • 26
  • 35
  • Just because your code is the same does not mean that the underlying request is formed identically. Unfortunately the underlying HTTP libraries provided by the platform can make a difference. You may need to break out a packet sniffer to analyze and compare the two requests. – Jason Apr 04 '18 at 02:39
  • I've tried Wireshark (using HTTP as the filter), Fiddler, etc. None of them are sniffing the packets. – David Apr 04 '18 at 02:58
  • Correction: I've tried Wireshark (using HTTP as the filter) and Charles (similar to Fiddler, but for Mac OS), and neither seem to be sniffing the packets. I assume it has something to do with it being an HTTPS connection, and not a plain HTTP connection. I have tried Fiddler's beta for Mac OS, but it isn't even running. – David Apr 04 '18 at 03:07
  • When running a MITM like Charles or Fiddler, you have to install the certificate of the proxy on the device/simulator and trust it. – David S. Apr 04 '18 at 03:34
  • Is the ToString() of the GET request identical between the platforms (except for the actual token)? Also, have you tried reading the Content of the failed response? – DavidS Apr 04 '18 at 04:51
  • Thanks for the advice about using a MITM like Charles. I re-opened Charles, had it install the certificates meant for the iOS simulator, and then I went onto the iOS simulator and went into the settings and enabled the option to trust the Charles certificate. I also went into Network Settings and turned on the "Web Proxy" and "Secure Web Proxy" settings, setting them to localhost on port 8080. I then ran my program again... but still nothing. Still not capturing the traffic from iOS simulator. Nor is it capturing anything from the console application. – David Apr 04 '18 at 05:24
  • Response to the 2nd DavidS that commented: The strings returned from the ToString() methods are identical except for the actual token. I have read the content of the failed response, and I edited my original question above to include the content of the failed response if you would like to see it. – David Apr 04 '18 at 05:51
  • Which HttpClient implementation are you using on the iOS project? If it isn't using NSUrlSession, that could be the problem. – DavidS Apr 05 '18 at 03:00
  • Looked into it. It was using the default managed HttpClient implementation. I switched it to use NSUrlSession - but not change in behavior unfortunately. – David Apr 05 '18 at 11:45

1 Answers1

0

Figured it out! I was finally able to get the Charles Proxy working so that I could snoop my HTTP SSL traffic coming from the iOS simulator. It turns out that my URL request initially resulted in a redirect, and when it tried to follow the redirect, the authorization header was stripped out of the request without me knowing.

The following Stack Overflow question helped me to solve the problem after I found it: Authorization header is lost on redirect

My code now looks something like this:

public async Task<int> GetCurrentPerson()
{
    var result = await _platform_host.GetAsync(_current_person_uri);

    //Check to see if the result was a "401 Unauthorized"
    if (result.StatusCode == HttpStatusCode.Unauthorized)
    {
        //If so, the "Authorization" header was likely stripped after a redirect
        //We will simply follow the redirect URL by calling GetAsync, and the
        //authorization header should be placed back on.

        //contains the final location after following the redirect.
        var finalRequestUri = result.RequestMessage.RequestUri; 

        //detect that a redirect actually did occur.
        if (finalRequestUri != _current_person_uri) 
        {
            // check that we can trust the host we were redirected to.
            if (IsHostTrusted(finalRequestUri)) 
            {
                // Reissue the request. The DefaultRequestHeaders configured 
                //on the client will be used, so we don't have to set them again.
                result = await _platform_host.GetAsync(finalRequestUri); 
            }
        }    
    }

    //Now check to see if the status is successful
    if (result.IsSuccessStatusCode)
    {
        ... handle JSON that was returned in response ...
    }

    ... rest of function ...
}

private bool IsHostTrusted(Uri uri)
{
    return (uri.Host == _host_address);
}
David
  • 1,847
  • 4
  • 26
  • 35