Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to make a valid GET call with HttpClient

I'm trying to write a simple console app to communicate to my Honeywell thermostat. They offer a free-to-use REST API that's documented here: https://developer.honeywellhome.com/ . I am having trouble making a simple GET call right after authenticating, and I don't know what I'm doing wrong. I was hoping someone could help me here.

Summarized, my process consists of 3 steps:

  1. Register an app to get an AppID and a Secret (all good here).
  2. Authenticate with OAuth2 to get an access token (all good here).
  3. Call any REST API using the provided access token (problem is here).

Details

My console csproj is very simple:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>true</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

1. Register an app to get an AppID and a Secret.

The app gets registed here, it's pretty standard: https://developer.honeywellhome.com/user/me/apps

For this example, let's assume Resideo gives me these values that I'll use later:

  • AppId: ABCD1234
  • Secret: WXYZ9876

2. Authenticate to get an access token.

The structure of the access token json response is described here: https://developer.honeywellhome.com/authorization-oauth2/apis/post/accesstoken

This is how I define the class for json deserialization:

internal class ResideoToken
{
    [JsonPropertyName("refresh_token_expires_in")]
    public string RefreshTokenExpiration { get; set; } = string.Empty;
    [JsonPropertyName("api_product_list")]
    public string ApiProductList { get; set; } = string.Empty;
    [JsonPropertyName("organization_name")]
    public string OrganizationName { get; set; } = string.Empty;
    [JsonPropertyName("developer.email")]
    public string DeveloperEmail { get; set; } = string.Empty;
    [JsonPropertyName("token_type")]
    public string TokenType { get; set; } = string.Empty;
    [JsonPropertyName("issued_at")]
    public string IssuedAt { get; set; } = string.Empty;
    [JsonPropertyName("client_id")]
    public string ClientId { get; set; } = string.Empty;
    [JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = string.Empty;
    [JsonPropertyName("application_name")]
    public string ApplicationName { get; set; } = string.Empty;
    [JsonPropertyName("scope")]
    public string Scope { get; set; } = string.Empty;
    [JsonPropertyName("expires_in")]
    public string ExpiresIn { get; set; } = string.Empty;
    [JsonPropertyName("refresh_count")]
    public string RefreshCount { get; set; } = string.Empty;
    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;
}

And this is how I'm successfully authenticating:

string appId = "ABCD1234";
string secret = "WXYZ9876";
using HttpClient client = new()
{
    BaseAddress = new Uri(uriString: "https://api.honeywell.com/", uriKind: UriKind.Absolute)
};

    KeyValuePair<string, string>[] encodedContentCollection =
[
    new("Content-Type", "application/x-www-form-urlencoded"),
    new("grant_type", "client_credentials")
];
using HttpRequestMessage request = new(HttpMethod.Post, "oauth2/accesstoken")
{
    Content = new FormUrlEncodedContent(encodedContentCollection)
};

string base64AppIdAndSecret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{appId}:{secret}"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64AppIdAndSecret);

using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode(); // Should throw if not 200-299

using Stream responseContentStream = await response.Content.ReadAsStreamAsync();

ResideoToken token = await JsonSerializer.DeserializeAsync<ResideoToken>(responseContentStream, JsonSerializerOptions.Default) ?? 
    throw new Exception("Could not deserialize response stream to a ResideoToken");

3. Call any REST API using the provided access token.

The simplest case I found is to get the list of locations and devices using GET method and passing one parameter: https://developer.honeywellhome.com/lyric/apis/get/locations

// I had this originally, but it was incorrect -> client.DefaultRequestHeaders.Add("Bearer", token.AccessToken);
client.DefaultRequestHeaders.Authentication = new AuthenticationHeaderValue("Bearer", token.AccessToken);

client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

// Base URL has already been established in the client
// According to the instructions, the apikey is the AppID
using HttpResponseMessage locationResponse = await client.GetAsync($"v2/locations?apikey={appId}");

locationResponse.EnsureSuccessStatusCode(); // This is failing with 401 unauthorized

// I am never able to reach this
string result = await locationResponse.Content.ReadAsStringAsync();
Console.WriteLine($"Locations: {result}");

As you can see, the GetAsync call fails with 401. This is the exception:

Unhandled exception. System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()

This is strange, because if I print the base64 string generated by my code to console, copy it, and use it in a curl call, it succeeds:

$ curl -X GET --header "Authorization: Bearer AbCdEfGhIjKlMnOpQrStUvWxYz==" "https://api.honeywell.com/v2/locations?apikey=ABCD1234"

# This prints a huge valid json file describing all the details of the locations and my devices as described in https://developer.honeywellhome.com/lyric/apis/get/locations

Questions

The 3rd step is where all the problems happen, so these are the things I'm unsure about:

  • Am I correctly reusing the access token string I got from the successful authentication?
  • Am I reusing the HttpClient correctly? It's my understanding that the recommendation is to keep using the same instance for the same group of requests identified with the same auth.
  • Am I setting the Bearer header in the right place as a default header, or should I manually create a request and set the header there? If the latter, how do I need to do it?
  • Am I setting the Accept media type of the default request header to a valid value of "application/json"? If not, where do I need to put it?
  • Do the default options originally set in the HttpClient when authenticating cause any issues with the subsequent request calls? In other words, do I need to clear the default headers for example?
  • Am I passing the correct url to the GetAsync call? It does not contain the base url.
  • Is it correct to set the GET parameter (apikey=1234ABCD) directly in the GetAsync url string? If not, what's the correct way?
  • Any suggestions on how to debug the 401 response, considering it did work when using curl?

Thanks in advance.

Edit: I fixed the line that sets the Bearer token but I am still getting the exact same 401 exception. Also added the missing usings.

like image 877
Carlos Sanchez Avatar asked Dec 28 '25 14:12

Carlos Sanchez


2 Answers

at the step 3 (Call any REST API using the provided access token) i see that you do this for adding the bearer token:

client.DefaultRequestHeaders.Add("Bearer", token.AccessToken);

I think you should instead do this to indicate you want to add authorization (as you were doing at step 2):

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
like image 148
n-eit Avatar answered Dec 30 '25 04:12

n-eit


The OAuth2 Client Credentials documentation mentions that in addition to the bearer token you also have to specify a UserRefID HTTP header:

If a user and device(s) have been authorized to your API Key, API calls can be made on the user's behalf by adding an HTTP Header called "UserRefID" and the value of the header is the UserID obtained from the API.

Example Request:

curl --location --request GET 'https://api.honeywell.com/v2/locations?apikey=(yourApiKey)'
--header 'UserRefID: 625115'
--header 'Authorization: Bearer (yourclientcredentialstoken)'

But I have no idea what API they are talking about when they say UserID obtained from the API. Maybe they are referring to the registration APIs?

Unfortunately it's pretty hard for someone without a physical device to help you because as mentioned in their FAQ, a physical device is required to use the APIs:

Yes, currently we do not have a device simulator, so you need to have a device or have access to an account with a device attached to use the API.

I have tried their interactive API on https://developer.honeywellhome.com/lyric/apis/get/locations but clicking on the OAuth 2.0 Set… button redirects to a resideo login page where I can't login. I think it's the same account as on https://account.honeywellhome.com (both mention a lyric thing) but I can't figure out how to create on account here. I have created an account on https://www.resideo.com but it looks like it's not the same thing!

like image 22
0xced Avatar answered Dec 30 '25 04:12

0xced



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!