-
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
|
👋 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 |
|
@jules please rebase this branch |
I have rebased the branch on the latest master and resolved the conflicts. |
- 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>
1e34b2d to
59f30ad
Compare
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
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.
| 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(); |
Copilot
AI
Jan 31, 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 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.
| public Authenticator(WithingsCredentials credentials) | ||
| { | ||
| _clientId = credentials.ClientId; | ||
| _clientSecret = credentials.ClientSecret; | ||
| _callbackUrl = credentials.CallbackUrl; | ||
| } |
Copilot
AI
Jan 31, 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 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.
| } | ||
|
|
||
| public async Task<AuthResponse> GetAccessToken(string code, string redirectUri = null) | ||
| { |
Copilot
AI
Jan 31, 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 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.
| { | |
| { | |
| if (string.IsNullOrWhiteSpace(code)) | |
| { | |
| throw new ArgumentException("Authorization code must not be null, empty, or whitespace.", nameof(code)); | |
| } |
| } | ||
|
|
||
| public async Task<AuthResponse> RefreshAccessToken(string refreshToken) | ||
| { |
Copilot
AI
Jan 31, 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 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.
| { | |
| { | |
| if (string.IsNullOrWhiteSpace(refreshToken)) | |
| { | |
| throw new ArgumentException("The refreshToken parameter must not be null, empty, or whitespace.", nameof(refreshToken)); | |
| } |
|
|
||
| // 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 31, 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 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.
| // 1491868800 |
| signature | ||
| }) | ||
| .ReceiveJson<ResponseWrapper<NonceResponse>>(); | ||
|
|
Copilot
AI
Jan 31, 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 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.
| 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."); | |
| } |
|
|
||
| ### Exchanging code for token | ||
| ```csharp | ||
| var authResponse = await authenticator.GetAccessToken("authorization_code"); |
Copilot
AI
Jan 31, 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 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.
| var authResponse = await authenticator.GetAccessToken("authorization_code"); | |
| // 'code' is the authorization code received from the OAuth callback | |
| var authResponse = await authenticator.GetAccessToken(code); |
| public WithingsClient() | ||
| { | ||
| _credentials = credentials; | ||
| } |
Copilot
AI
Jan 31, 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 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.
| } No newline at end of file | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; |
Copilot
AI
Jan 31, 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 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.
| using System.Linq; |
| } | ||
| } No newline at end of file | ||
| using System; | ||
| using System.Collections.Generic; |
Copilot
AI
Jan 31, 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 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.
| using System.Collections.Generic; |
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