Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 32 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

.NET Client for interacting with Withing OAuth1 Api
.NET Client for interacting with Withings API v2 (OAuth 2.0)

[![Build status](https://ci.appveyor.com/api/projects/status/lw9pd7gbdjgck3sq?svg=true)](https://ci.appveyor.com/project/atbyrd/withings-net)
[![Coverage Status](https://coveralls.io/repos/github/atbyrd/Withings.NET/badge.svg?branch=master)](https://coveralls.io/github/atbyrd/Withings.NET?branch=master)
Expand All @@ -9,57 +9,46 @@
[![NuGet](https://img.shields.io/nuget/v/Nuget.Core.svg?style=plastic)](https://www.nuget.org/packages/Withings.NET)

## USAGE
Due to external dependencies, your callback url should include a username param i.e. http://localhost:49294/api/oauth/callback/{username}

### All examples will use the Nancy Framework
### Authorization - Getting user authorization url
```csharp
var credentials = new WithingsCredentials();
credentials.SetClientProperties("client_id", "client_secret");
credentials.SetCallbackUrl("http://localhost:49294/callback");

#### Authorization - Getting user authorization url
var authenticator = new Authenticator(credentials);
var url = authenticator.GetAuthorizeUrl("state", "user.info,user.metrics");
// Redirect user to url
```
Get["api/oauth/authorize", true] = async (nothing, ct) =>
{
var url = await authenticator.UserRequstUrl(requestToken).ConfigureAwait(true);
new JsonRespons(url, new DefaultJsonSerializer());
}

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

### Fetching Data
```csharp
var client = new WithingsClient();
var data = await client.GetActivityMeasures(DateTime.Now.AddDays(-1), DateTime.Now, "userid", accessToken);
```

## CHANGE LOG

Version: 3.0.0 |
Release Date: Current |
Breaking Changes |
Migrated to OAuth 2.0.
Removed OAuth 1.0 support.
Updated project to SDK style (netstandard2.0).
Renamed `ConsumerKey`/`ConsumerSecret` to `ClientId`/`ClientSecret`.
Methods in `WithingsClient` now accept `accessToken` instead of `token` and `secret`.
Removed `AsyncOAuth` dependency.
Added `Flurl.Http` 3.x+ support.

Version: 2.1.0 |
Release Date: April 03, 2017 |
New Features |
Get Ability To Get Body Measures

Version: 2.0.0 |
Release Date: April 03, 2017 |
Breaking API Change |
GetActivityMeasures Now Accepts DateTimes Instead of Strings for Dates

Version: 1.1.29 |
Release Date:April 02, 2017 |
New Features |
Add Abiltity To Get Sleep Measures

Version: 1.1.27 |
Release Date:April 02, 2017 |
New Features |
Add Abiltity To Get Workout Data

Version: 1.1.26 |
Release Date:April 02, 2017 |
New Features |
Add Abiltity To Get Sleep Summary Over A Range Of Days

Version: 1.1.23 |
Release Date:April 01, 2017 |
New Features |
Add Abiltity To Get Activity Measures For A Specific Day

Version: 1.1.0 |
Release Date:April 01, 2017 |
New Features |
Add Abiltity To Get Activity Measures For A Date Range

Version: 1.0.0 |
Release Date:March 06, 2017 |
New Features |
Complete Authorization Process
...
Binary file removed UpgradeLog.htm
Binary file not shown.
198 changes: 139 additions & 59 deletions Withings.NET/Client/Authenticator.cs
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;
Comment on lines +59 to +61
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.
}

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
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.
}

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
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.
}

private string GenerateSignature(string action, string clientId, object thirdParam)
{
// For getnonce: action, client_id, timestamp
Comment on lines +106 to +108
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.
// 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.

Comment on lines +108 to +118
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 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.

Suggested change
// 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).

Copilot uses AI. Check for mistakes.
var data = $"{action},{clientId},{thirdParam}";

using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_clientSecret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}

private class ResponseWrapper<T>
{
public int Status { get; set; }
public T Body { get; set; }
}

private class NonceResponse
{
public string Nonce { get; set; }
}
}
}
Loading
Loading