-
Notifications
You must be signed in to change notification settings - Fork 1
Update to Withings API v2 and OAuth 2.0 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Update to Withings API v2 and OAuth 2.0 #5
Conversation
- 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.
|
👋 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 New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this 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/ConsumerSecrettoClientId/ClientSecretto 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.
| // 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 |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| // 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); |
| 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") |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| .ReceiveJson<ResponseWrapper<AuthResponse>>(); | ||
|
|
||
| return response.Body; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| .ReceiveJson<ResponseWrapper<NonceResponse>>(); | ||
|
|
||
| return response.Body.Nonce; |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| [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. | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| 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); | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| public WithingsClient() | ||
| { | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| public async Task<ExpandoObject> GetBodyMeasures(string userid, DateTime lastupdate, string accessToken) | ||
| { | ||
| // Original code used v1 for this endpoint | ||
| var query = BaseUri.AppendPathSegment("measure") |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| <TargetFramework>net8.0</TargetFramework> | |
| <TargetFramework>net6.0</TargetFramework> |
| private string GenerateSignature(string action, string clientId, object thirdParam) | ||
| { | ||
| // For getnonce: action, client_id, timestamp |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| 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 |
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.NETproject migrated to SDK style (netstandard2.0).Authenticatorrewritten to implement OAuth 2.0 flow (Authorize URL, Access Token, Refresh Token) with the required custom signature generation.WithingsClientupdated to use Bearer tokens and cleaner API calls using Flurl.WithingsCredentialsrenamed ConsumerKey/Secret to ClientId/Secret.GetJsonAsync()returning dynamic caused invalid cast exceptions; now usingGetJsonAsync<ExpandoObject>().PR created automatically by Jules for task 9542668834228568968 started by @antarr