-
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.Linq; | ||||||||||||||||||||||||||||||||||||||
| using System.Security.Cryptography; | ||||||||||||||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||
| using Flurl; | ||||||||||||||||||||||||||||||||||||||
| using Flurl.Http; | ||||||||||||||||||||||||||||||||||||||
| using Withings.NET.Models; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| namespace Withings.NET.Client | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| public class Authenticator | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| readonly string _clientId; | ||||||||||||||||||||||||||||||||||||||
| readonly string _clientSecret; | ||||||||||||||||||||||||||||||||||||||
| readonly string _callbackUrl; | ||||||||||||||||||||||||||||||||||||||
| const string BaseUri = "https://wbsapi.withings.net/v2"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public Authenticator(WithingsCredentials credentials) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| _clientId = credentials.ClientId; | ||||||||||||||||||||||||||||||||||||||
| _clientSecret = credentials.ClientSecret; | ||||||||||||||||||||||||||||||||||||||
| _callbackUrl = credentials.CallbackUrl; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public async Task<AuthResponse> GetAccessToken(string code, string redirectUri = null) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var nonce = await GetNonce(); | ||||||||||||||||||||||||||||||||||||||
| var action = "requesttoken"; | ||||||||||||||||||||||||||||||||||||||
| var grantType = "authorization_code"; | ||||||||||||||||||||||||||||||||||||||
| var rUri = redirectUri ?? _callbackUrl; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var signature = GenerateSignature(action, _clientId, nonce); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var response = await (BaseUri + "/oauth2") | ||||||||||||||||||||||||||||||||||||||
| .PostUrlEncodedAsync(new | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| action, | ||||||||||||||||||||||||||||||||||||||
| client_id = _clientId, | ||||||||||||||||||||||||||||||||||||||
| grant_type = grantType, | ||||||||||||||||||||||||||||||||||||||
| code, | ||||||||||||||||||||||||||||||||||||||
| redirect_uri = rUri, | ||||||||||||||||||||||||||||||||||||||
| nonce, | ||||||||||||||||||||||||||||||||||||||
| signature | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| .ReceiveJson<ResponseWrapper<AuthResponse>>(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return response.Body; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| public async Task<AuthResponse> RefreshAccessToken(string refreshToken) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var nonce = await GetNonce(); | ||||||||||||||||||||||||||||||||||||||
| var action = "requesttoken"; | ||||||||||||||||||||||||||||||||||||||
| var grantType = "refresh_token"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var signature = GenerateSignature(action, _clientId, nonce); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var response = await (BaseUri + "/oauth2") | ||||||||||||||||||||||||||||||||||||||
| .PostUrlEncodedAsync(new | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| action, | ||||||||||||||||||||||||||||||||||||||
| client_id = _clientId, | ||||||||||||||||||||||||||||||||||||||
| grant_type = grantType, | ||||||||||||||||||||||||||||||||||||||
| refresh_token = refreshToken, | ||||||||||||||||||||||||||||||||||||||
| nonce, | ||||||||||||||||||||||||||||||||||||||
| signature | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| .ReceiveJson<ResponseWrapper<AuthResponse>>(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return response.Body; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+82
to
+84
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private async Task<string> GetNonce() | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var action = "getnonce"; | ||||||||||||||||||||||||||||||||||||||
| var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); | ||||||||||||||||||||||||||||||||||||||
| var signature = GenerateSignature(action, _clientId, timestamp); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var response = await (BaseUri + "/signature") | ||||||||||||||||||||||||||||||||||||||
| .PostUrlEncodedAsync(new | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| action, | ||||||||||||||||||||||||||||||||||||||
| client_id = _clientId, | ||||||||||||||||||||||||||||||||||||||
| timestamp, | ||||||||||||||||||||||||||||||||||||||
| signature | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| .ReceiveJson<ResponseWrapper<NonceResponse>>(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return response.Body.Nonce; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+103
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private string GenerateSignature(string action, string clientId, object thirdParam) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| // For getnonce: action, client_id, timestamp | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+108
|
||||||||||||||||||||||||||||||||||||||
| 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.
There's no error handling for the Withings API response status codes. The
ResponseWrapper<T>has aStatusfield, but it's never checked. According to Withings API documentation, a non-zero status indicates an error. The code should validate thatresponse.Status == 0before returningresponse.Body, otherwise it may return null or incomplete data when the API returns an error.