Skip to content

Conversation

@google-labs-jules
Copy link
Contributor

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

@google-labs-jules
Copy link
Contributor 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
Contributor

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.
@antarr
Copy link
Owner

antarr commented Jan 27, 2026

@jules please rebase this branch

@google-labs-jules
Copy link
Contributor Author

@jules please rebase this branch

I have rebased the branch on the latest master and resolved the conflicts.

google-labs-jules bot and others added 2 commits January 30, 2026 18:59
- 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.
- 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.
- Rebased on master and resolved conflicts.

Co-authored-by: antarr <974295+antarr@users.noreply.github.com>
@antarr antarr force-pushed the update-withings-api-oauth2-9542668834228568968 branch from 1e34b2d to 59f30ad Compare January 31, 2026 00:59
@antarr antarr requested a review from Copilot January 31, 2026 00:59
Copy link
Contributor

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

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


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

Comment on lines +27 to +36
public string GetAuthorizeUrl(string state, string scope, string redirectUri = null)
{
var uri = "https://account.withings.com/oauth2_user/authorize2"
.SetQueryParam("response_type", "code")
.SetQueryParam("client_id", _clientId)
.SetQueryParam("scope", scope)
.SetQueryParam("state", state)
.SetQueryParam("redirect_uri", redirectUri ?? _callbackUrl);

return uri.ToString();
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The GetAuthorizeUrl method does not validate that the required parameters (state and scope) are not null or empty before constructing the authorization URL. Consider adding validation to ensure these parameters are provided, as they are required for a valid OAuth 2.0 authorization request.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +25
public Authenticator(WithingsCredentials credentials)
{
_clientId = credentials.ClientId;
_clientSecret = credentials.ClientSecret;
_callbackUrl = credentials.CallbackUrl;
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The Authenticator constructor does not validate that the credentials object or its properties (ClientId, ClientSecret, CallbackUrl) are not null or empty. If any of these are null, the methods will fail later with unclear error messages. Consider adding validation in the constructor to fail fast with clear error messages.

Copilot uses AI. Check for mistakes.
}

public async Task<AuthResponse> GetAccessToken(string code, string redirectUri = null)
{
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The GetAccessToken method does not validate that the 'code' parameter is not null or empty before making the API request. Consider adding validation to prevent API calls with invalid parameters.

Suggested change
{
{
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("Authorization code must not be null, empty, or whitespace.", nameof(code));
}

Copilot uses AI. Check for mistakes.
}

public async Task<AuthResponse> RefreshAccessToken(string refreshToken)
{
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The RefreshAccessToken method does not validate that the 'refreshToken' parameter is not null or empty before making the API request. Consider adding validation to prevent API calls with invalid parameters.

Suggested change
{
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
throw new ArgumentException("The refreshToken parameter must not be null, empty, or whitespace.", nameof(refreshToken));
}

Copilot uses AI. Check for mistakes.

// 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 31, 2026

Choose a reason for hiding this comment

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

The inline comment on line 41 is redundant - the value 1491868800 is already specified in the assertion on line 42. This adds no value and should be removed.

Suggested change
// 1491868800

Copilot uses AI. Check for mistakes.
signature
})
.ReceiveJson<ResponseWrapper<NonceResponse>>();

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The GetNonce method does not validate the response status before returning the nonce. If the Withings API returns an error status (non-zero), the Body or Body.Nonce might be null. Consider checking response.Status and handling error cases appropriately before returning response.Body.Nonce.

Suggested change
if (response == null)
{
throw new InvalidOperationException("Failed to get nonce: response was null.");
}
if (response.Status != 0)
{
throw new InvalidOperationException($"Failed to get nonce: API returned status {response.Status}.");
}
if (response.Body == null || string.IsNullOrEmpty(response.Body.Nonce))
{
throw new InvalidOperationException("Failed to get nonce: response body or nonce was null or empty.");
}

Copilot uses AI. Check for mistakes.

### Exchanging code for token
```csharp
var authResponse = await authenticator.GetAccessToken("authorization_code");
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The README example on line 26 shows "authorization_code" as the parameter to GetAccessToken, but this should be the actual authorization code value received from the OAuth callback (e.g., a variable like 'code' or 'authCode'). The literal string "authorization_code" is the grant_type, not the code parameter. This could confuse developers trying to use the library.

Suggested change
var authResponse = await authenticator.GetAccessToken("authorization_code");
// 'code' is the authorization code received from the OAuth callback
var authResponse = await authenticator.GetAccessToken(code);

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

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The empty constructor body (lines 13-15) serves no purpose and can be removed. In C#, if you don't need to perform any initialization, you can omit the constructor entirely and rely on the default constructor.

Copilot uses AI. Check for mistakes.
} No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The using directive "using System.Linq;" on line 3 is imported but never used in this file. It should be removed to keep the code clean.

Suggested change
using System.Linq;

Copilot uses AI. Check for mistakes.
}
} No newline at end of file
using System;
using System.Collections.Generic;
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The using directive "using System.Collections.Generic;" on line 2 is imported but never used in this file. It should be removed to keep the code clean.

Suggested change
using System.Collections.Generic;

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