Skip to content

Conversation

@google-labs-jules
Copy link

This PR updates the package to support the current Withings API v2 and OAuth 2.0 authentication, as the previous version hasn't been updated in years and relied on deprecated OAuth 1.0.

Key changes:

  • Withings.NET project migrated to SDK style (netstandard2.0).
  • Authenticator rewritten to implement OAuth 2.0 flow (Authorize URL, Access Token, Refresh Token) with the required custom signature generation.
  • WithingsClient updated to use Bearer tokens and cleaner API calls using Flurl.
  • WithingsCredentials renamed ConsumerKey/Secret to ClientId/Secret.
  • Tests updated to verify the new authentication flow using mocks.
  • Fixed a runtime issue where Flurl's GetJsonAsync() returning dynamic caused invalid cast exceptions; now using GetJsonAsync<ExpandoObject>().

PR created automatically by Jules for task 9542668834228568968 started by @antarr

- Migrated from OAuth 1.0 to OAuth 2.0.
- Implemented custom HMAC-SHA256 signature generation for Withings API.
- Updated project to SDK style targeting netstandard2.0.
- Replaced AsyncOAuth with Flurl.Http for authentication flows.
- Updated WithingsClient to use Bearer token authentication.
- Updated tests to use Flurl.Http.Testing and verified OAuth 2.0 flow.
- Removed legacy dependencies and files.
- Fixed invalid cast exceptions in WithingsClient by using strong typing.
@google-labs-jules
Copy link
Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request modernizes the Withings.NET library by migrating from the deprecated OAuth 1.0 and API v1 to OAuth 2.0 and Withings API v2. The PR transforms the project structure from traditional .NET Framework to SDK-style projects, targeting netstandard2.0 for broader compatibility.

Changes:

  • Migrated authentication from OAuth 1.0 (using AsyncOAuth library) to OAuth 2.0 with custom HMAC-SHA256 signature generation
  • Updated all API client methods to use Bearer token authentication instead of OAuth 1.0 signatures
  • Converted project files to SDK-style format and upgraded dependencies (Flurl.Http 3.2.4, Newtonsoft.Json 13.0.3)
  • Renamed ConsumerKey/ConsumerSecret to ClientId/ClientSecret to align with OAuth 2.0 terminology

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
Withings.NET/Withings.csproj Migrated to SDK-style project targeting netstandard2.0 with updated dependencies
Withings.NET/Client/Authenticator.cs Completely rewritten to implement OAuth 2.0 flow with custom signature generation for Withings API v2
Withings.NET/Client/WithingsClient.cs Simplified to use Bearer tokens; removed OAuth 1.0 signature logic; updated endpoints to v2
Withings.NET/Client/OAuthBase.cs Removed - no longer needed with OAuth 2.0
Withings.NET/Models/WithingsCredentials.cs Renamed properties from ConsumerKey/Secret to ClientId/Secret
Withings.NET/Models/AuthResponse.cs New model for OAuth 2.0 token responses
Withings.Specifications/Withings.Net.Tests.csproj Migrated to SDK-style project targeting net8.0 with modern test dependencies
Withings.Specifications/AuthenticatorTests.cs Rewritten tests for OAuth 2.0 flow using Flurl.Http.Testing mocks
Withings.Specifications/WithingsClientTests.cs New test file for client methods
Withings.Specifications/DateTimeExtensionsTests.cs Fixed test assertions and Unix timestamp values for accuracy
README.md Updated documentation with OAuth 2.0 examples and changelog for version 3.0.0
packages.config files Removed - replaced by PackageReference in SDK-style projects
Properties/AssemblyInfo.cs files Removed - metadata now auto-generated by SDK-style projects

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +41
// Note: 04/11/2017 in UTC might depend on local time if not specified,
// but DateTime.Parse uses local time, and ToUnixTime in code uses UTC epoch.
// The code in DateTimeExtensions.cs:
// return Convert.ToInt64((date - epoch).TotalSeconds);
// epoch is UTC.
// If date is Unspecified (from Parse), subtraction treats it as same kind (or assumes local?).

// Let's use a fixed UTC date to be sure.
var date = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc);
// 1491868800
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These verbose comments about DateTime parsing and Unix time conversion should be removed or condensed. They appear to be debugging notes rather than useful documentation. If the concern is about timezone handling, a concise comment would suffice.

Suggested change
// Note: 04/11/2017 in UTC might depend on local time if not specified,
// but DateTime.Parse uses local time, and ToUnixTime in code uses UTC epoch.
// The code in DateTimeExtensions.cs:
// return Convert.ToInt64((date - epoch).TotalSeconds);
// epoch is UTC.
// If date is Unspecified (from Parse), subtraction treats it as same kind (or assumes local?).
// Let's use a fixed UTC date to be sure.
var date = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc);
// 1491868800
// Use a fixed UTC date to avoid timezone and local parsing ambiguity.
var date = new DateTime(2017, 4, 11, 0, 0, 0, DateTimeKind.Utc);

Copilot uses AI. Check for mistakes.
public async Task<ExpandoObject> GetBodyMeasures(string userid, DateTime start, DateTime end, string accessToken)
{
// Original code used v1 for this endpoint
var query = BaseUri.AppendPathSegment("measure")
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates this is a v1 endpoint ("Original code used v1 for this endpoint"), but the BaseUri is set to "https://wbsapi.withings.net" and the path is just "measure" (not "v2/measure" like other methods). However, this creates an inconsistency where all other endpoints use "v2/" in their paths, but this one doesn't. According to Withings API v2 documentation, the getmeas endpoint should likely be "v2/measure" for consistency, or the comment should clarify why v1 is still being used if that's intentional.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +84
.ReceiveJson<ResponseWrapper<AuthResponse>>();

return response.Body;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no error handling for the Withings API response status codes. The ResponseWrapper<T> has a Status field, but it's never checked. According to Withings API documentation, a non-zero status indicates an error. The code should validate that response.Status == 0 before returning response.Body, otherwise it may return null or incomplete data when the API returns an error.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +103
.ReceiveJson<ResponseWrapper<NonceResponse>>();

return response.Body.Nonce;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no error handling for the Withings API response status codes. The ResponseWrapper<T> has a Status field, but it's never checked. According to Withings API documentation, a non-zero status indicates an error. The code should validate that response.Status == 0 before returning response.Body.Nonce, otherwise it may return null or incomplete data when the API returns an error.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +52
[Test]
public async Task GetActivityMeasuresTest()
{
_httpTest.RespondWithJson(new { status = 0, body = new { some_data = "test" } });

var start = DateTime.UtcNow.Date;
var end = DateTime.UtcNow.Date.AddDays(1);

dynamic result = await _client.GetActivityMeasures(start, end, "userid", "access_token");

((object)result).Should().BeOfType<ExpandoObject>();
// Verify property existence and value (JSON numbers are typically long/int64)
long status = result.status;
status.Should().Be(0);

// Wait, RespondWithJson serializes the object. GetJsonAsync<ExpandoObject> deserializes it.
// ExpandoObject will have properties matching the json.
// result is ExpandoObject.
// But checking properties on ExpandoObject directly needs casting to IDictionary<string, object> or using dynamic.
// "status" and "body" should be present.
}
}
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WithingsClientTests only includes a test for GetActivityMeasures, but the WithingsClient class has many other methods (GetSleepSummary, GetSleepMeasures, GetWorkouts, GetIntraDayActivity, GetBodyMeasures). Since other test files in this project have comprehensive test coverage, all public methods should have corresponding tests to maintain consistency with the project's testing standards.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +29
public async Task<ExpandoObject> GetActivityMeasures(DateTime startDay, DateTime endDay, string userId, string accessToken)
{
var query = BaseUri.AppendPathSegment("v2/measure")
.SetQueryParam("action", "getactivity")
.SetQueryParam("userid", userId)
.SetQueryParam("startdateymd", $"{startDay:yyyy-MM-dd}")
.SetQueryParam("enddateymd", $"{endDay:yyyy-MM-dd}")
.WithOAuthBearerToken(accessToken);

return await query.GetJsonAsync<ExpandoObject>().ConfigureAwait(false);
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WithingsClient methods return ExpandoObject which likely contains a Withings API response with a status field. However, there's no validation or documentation about checking the status field before using the response data. Consider adding error handling to check the response status, or at minimum, document that callers need to check the status field themselves.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
public WithingsClient()
{
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor no longer requires WithingsCredentials as a parameter (it's now parameterless), but all the methods now require an accessToken parameter. This is a breaking change from the previous version where credentials were passed to the constructor and used internally. While this is documented in the README changelog, consider whether this API design makes sense - if credentials are no longer needed by the client, the parameterless constructor is appropriate. However, ensure this design is intentional and not an oversight.

Copilot uses AI. Check for mistakes.
public async Task<ExpandoObject> GetBodyMeasures(string userid, DateTime lastupdate, string accessToken)
{
// Original code used v1 for this endpoint
var query = BaseUri.AppendPathSegment("measure")
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates this is a v1 endpoint ("Original code used v1 for this endpoint"), but the BaseUri is set to "https://wbsapi.withings.net" and the path is just "measure" (not "v2/measure" like other methods). This creates an inconsistency where all other endpoints use "v2/" in their paths, but this one doesn't. According to Withings API v2 documentation, the getmeas endpoint should likely be "v2/measure" for consistency, or the comment should clarify why v1 is still being used if that's intentional.

Copilot uses AI. Check for mistakes.
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test project targets net8.0 while the main library targets netstandard2.0. This is acceptable, but could cause issues if developers want to run tests on older .NET versions. Consider targeting a lower .NET version for tests (e.g., net6.0 or net7.0) if broader compatibility is needed, or ensure this is intentional for using latest testing features.

Suggested change
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +108
private string GenerateSignature(string action, string clientId, object thirdParam)
{
// For getnonce: action, client_id, timestamp
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature generation uses object type for thirdParam and directly interpolates it into a string. While this works for timestamp (long) and nonce (string), using object type here reduces type safety. Consider using method overloads with specific types (string for nonce, long for timestamp) to make the API more explicit and type-safe, or at minimum add a runtime type check to ensure only expected types are passed.

Suggested change
private string GenerateSignature(string action, string clientId, object thirdParam)
{
// For getnonce: action, client_id, timestamp
private string GenerateSignature(string action, string clientId, string nonce)
{
// For requesttoken: action, client_id, nonce
return GenerateSignatureCore(action, clientId, nonce);
}
private string GenerateSignature(string action, string clientId, long timestamp)
{
// For getnonce: action, client_id, timestamp
return GenerateSignatureCore(action, clientId, timestamp.ToString());
}
private string GenerateSignatureCore(string action, string clientId, string thirdParam)
{
// For getnonce: action, client_id, timestamp

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant