-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,59 +1,139 @@ | ||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||
| using System.Runtime.CompilerServices; | ||||||||||||||||||||||||||||||||||||||
| using System.Security.Cryptography; | ||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||
| using AsyncOAuth; | ||||||||||||||||||||||||||||||||||||||
| using Withings.NET.Models; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| [assembly: InternalsVisibleTo("Withings.Net.Specifications")] | ||||||||||||||||||||||||||||||||||||||
| namespace Withings.NET.Client | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| public class Authenticator | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| readonly string _consumerKey; | ||||||||||||||||||||||||||||||||||||||
| readonly string _consumerSecret; | ||||||||||||||||||||||||||||||||||||||
| readonly string _callbackUrl; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public Authenticator(WithingsCredentials credentials) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| _consumerKey = credentials.ConsumerKey; | ||||||||||||||||||||||||||||||||||||||
| _consumerSecret = credentials.ConsumerSecret; | ||||||||||||||||||||||||||||||||||||||
| _callbackUrl = credentials.CallbackUrl; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| OAuthUtility.ComputeHash = (key, buffer) => { using (var hmac = new HMACSHA1(key)) { return hmac.ComputeHash(buffer); } }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public async Task<RequestToken> GetRequestToken() | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); | ||||||||||||||||||||||||||||||||||||||
| var parameters = new List<KeyValuePair<string, string>> | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| new KeyValuePair<string, string>("oauth_callback", Uri.EscapeUriString(_callbackUrl)) | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| TokenResponse<RequestToken> tokenResponse = await authorizer.GetRequestToken("https://oauth.withings.com/account/request_token", parameters); | ||||||||||||||||||||||||||||||||||||||
| return tokenResponse.Token; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||
| /// GET USER REQUEST URL | ||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||
| /// <returns>string</returns> | ||||||||||||||||||||||||||||||||||||||
| public string UserRequestUrl(RequestToken token) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); | ||||||||||||||||||||||||||||||||||||||
| return authorizer.BuildAuthorizeUrl("https://oauth.withings.com/account/authorize", token); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||
| /// GET USER ACCESS TOKEN | ||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||
| /// <returns>OAuth1Credentials</returns> | ||||||||||||||||||||||||||||||||||||||
| public async Task<AccessToken> ExchangeRequestTokenForAccessToken(RequestToken requestToken, string oAuthVerifier) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var authorizer = new OAuthAuthorizer(_consumerKey, _consumerSecret); | ||||||||||||||||||||||||||||||||||||||
| TokenResponse<AccessToken> accessTokenResponse = await authorizer.GetAccessToken("https://oauth.withings.com/account/access_token", requestToken, oAuthVerifier); | ||||||||||||||||||||||||||||||||||||||
| return accessTokenResponse.Token; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| 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.Linq;" on line 3 is imported but never used in this file. It should be removed to keep the code clean.
| 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 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
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.
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)); | |
| } |
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.
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)); | |
| } |
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.
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."); | |
| } |
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.
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 |
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 explaining the signature generation logic should be condensed. The explanation spans multiple lines but could be summarized more concisely, or moved to proper documentation if needed.
| // For getnonce: action, client_id, timestamp | |
| // For requesttoken: action, client_id, nonce | |
| // The documentation says "Concatenate the sorted values (alphabetically by key name)". | |
| // In both cases, the keys are: | |
| // 1. action | |
| // 2. client_id | |
| // 3. timestamp OR nonce | |
| // alphabetically: action, client_id, nonce OR action, client_id, timestamp | |
| // So the order is always action, clientId, thirdParam. | |
| // Signature data is the comma-separated values of action, client_id, and timestamp/nonce (sorted by key name). |
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.