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:
- Register an app to get an AppID and a Secret (all good here).
- Authenticate with OAuth2 to get an access token (all good here).
- 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";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")];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);HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode(); // Should throw if not 200-299Stream 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 AppIDHttpResponseMessage locationResponse = await client.GetAsync($"v2/locations?apikey={appId}");locationResponse.EnsureSuccessStatusCode(); // This is failing with 401 unauthorized// I am never able to reach thisstring 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.