Reputation: 1976
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!
Upvotes: 1
Views: 796
Reputation: 1976
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);
}
Upvotes: 0