Withings API - 签名无效
Withings API - Invalid signature
我正在尝试通过 oauth(.net 核心 C#)访问我的 Withings/Nokia 缩放数据。
说明可在以下位置找到:
https://oauth.withings.com/en/api/oauthguide
和API指南在这里:
https://developer.health.nokia.com/api#step1
我已完成第 1 部分 - 我获得了授权令牌和密码。
第 2 部分 - 我已手动检索到授权我的应用程序使用我的 Withings 秤数据的代码 - 即作为回调结果的授权代码(通过 API 开发人员页面)。我假设这只需要完成一次就可以永久授权我的应用程序访问。我已将此值硬编码到我的代码中,并在我重新授权应用程序时更新它。
现在我卡在第 3 部分 - 获取访问权限 token/secret。
错误 = 无效签名
(使用上面的页面,我已经能够检索到我 4 年的体重秤数据,所以我知道它应该有效)。
我的基本签名与上述 API 测试页相同(随机数、签名和时间戳除外)。
我的 url 与上面的 API 测试页相同(除了 nonce 和时间戳)。
我不解的是为什么这适用于第 1 部分而不适用于第 3 部分。
是代码有问题还是仅仅是请求令牌在发出请求之前必须针对 application/users 数据进行授权?
不过肯定不用每次都跟用户重新授权吧??
我最初搞砸了第 1 部分并给出了无效签名错误 - 这显然是签名的问题 - 但我重新检查了第 3 部分中的签名并且它很好。
private const string AUTH_VERSION = "1.0";
private const string SIGNATURE_METHOD = "HMAC-SHA1";
private const string BASE_URL_REQUEST_AUTH_TOKEN = "https://developer.health.nokia.com/account/request_token";
private const string BASE_URL_REQUEST_ACCESS_TOKEN = "https://developer.health.nokia.com/account/access_token";
...
Withings w = new Withings();
OAuthToken t = await w.GetOAuthToken();
string token = t.OAuth_Token;
string secret = t.OAuth_Token_Secret;
OAuthAccessToken at = await w.GetOAuthAccess(t);
string aToken = at.OAuth_Token;
string aTokenSecret = at.OAuth_Token_Secret;
...
public async Task<OAuthAccessToken> GetOAuthAccess(OAuthToken authToken)
{
OAuthAccessToken token = new OAuthAccessToken();
try
{
string random = GetRandomString();
string timestamp = GetTimestamp();
string baseSignature = GetOAuthAccessSignature(authToken, random, timestamp);
string hashSignature = ComputeHash(baseSignature, CONSUMER_SECRET, authToken.OAuth_Token_Secret);
string codeSignature = UrlEncode(hashSignature);
string requestUrl = GetOAuthAccessUrl(authToken, codeSignature, random, timestamp);
HttpResponseMessage response = await client.GetAsync(requestUrl);
string responseBodyAsText = await response.Content.ReadAsStringAsync();
string[] parameters = responseBodyAsText.Split('&');
token.OAuth_Token = parameters[0].Split('=')[1].ToString();
token.OAuth_Token_Secret = parameters[1].Split('=')[1].ToString();
}
catch (Exception ex)
{
}
return token;
}
private string GetOAuthAccessSignature(OAuthToken authToken, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
//{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", END_USER_AUTHORISATION_REQUEST_TOKEN },
{ "oauth_version", AUTH_VERSION}
};
StringBuilder sb = new StringBuilder();
sb.Append("GET&" + UrlEncode(BASE_URL_REQUEST_ACCESS_TOKEN) + "&oauth_consumer_key%3D" + CONSUMER_KEY);
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append(UrlEncode("&"));
sb.Append(UrlEncode(urlItem.Key + "=" + urlItem.Value));
}
return sb.ToString();
}
private string GetOAuthAccessUrl(OAuthToken authToken, string signature, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature", signature },
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", END_USER_AUTHORISATION_REQUEST_TOKEN },
{ "oauth_version", AUTH_VERSION}
};
StringBuilder sb = new StringBuilder();
sb.Append(BASE_URL_REQUEST_ACCESS_TOKEN + "?");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count > 1) sb.Append("&");
sb.Append(urlItem.Key + "=" + urlItem.Value);
}
return sb.ToString();
}
备注:
- 我已经订购了我的参数
- 我有一个不错的url编码(纠正我错误)
- 我有一个 hmac-sha1 散列(纠正我错误)
- 对使用开放库不感兴趣 - 我想在没有第三方工具的情况下修复此代码
以下是我正在使用的辅助方法:
private string ComputeHash(string data, string consumerSecret, string tokenSecret = null)
{
// Construct secret key based on consumer key (and optionally include token secret)
string secretKey = consumerSecret + "&";
if (tokenSecret != null) secretKey += tokenSecret;
// Initialise with secret key
System.Security.Cryptography.HMACSHA1 hmacsha = new System.Security.Cryptography.HMACSHA1(Encoding.ASCII.GetBytes(secretKey));
hmacsha.Initialize();
// Convert data into byte array
byte[] dataBuffer = Encoding.ASCII.GetBytes(data);
// Computer hash of data byte array
byte[] hashBytes = hmacsha.ComputeHash(dataBuffer);
// Return the base 64 of the result
return Convert.ToBase64String(hashBytes);
}
// Get random string
private string GetRandomString()
{
return Guid.NewGuid().ToString().Replace("-", "");
}
// Get timestamp
private string GetTimestamp()
{
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
// Url Encode (as Uri.Escape is reported to be not appropriate for this purpose)
protected string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
protected string UrlEncode(string value)
{
var result = new StringBuilder();
foreach (var symbol in value)
{
if (UnreservedChars.IndexOf(symbol) != -1)
result.Append(symbol);
else
result.Append('%' + $"{(int)symbol:X2}");
}
return result.ToString();
}
谢谢,丹。
已解决 - 这是我对 oauth 工作原理的理解的问题。
第 1 步 - 获取令牌(这允许您根据您的 api 帐户申请提出请求)
第 2 步 - 创建一个 URL(使用上面的 2 分钟令牌)重定向用户以授权您的 Withings api 应用程序使用特定用户的帐户。当您传递它时返回相同的令牌 - 但现在将允许它在步骤 3 中发出请求。
第 3 步 - 请求访问令牌 - 这将为您提供一个令牌和秘密字符串,允许您继续访问此用户的帐户(对于您的 api 帐户申请)。
第 4 步 - 请求数据 - 方法与前面所有步骤类似 - 非常简单。 Returns一大串数据。阅读 API 文档,您可以过滤 - 这就是我将要做的,因为我有大约 4/5 年的 'interesting' 体重数据。
我在执行第 1 步然后执行第 3 步时认为从第 2 步返回的代码(没有注意到它与输入的代码相同)可以存储并用于第 3 步而无需重新授权。
您实际上(以及我所做的)是按照 API 演示界面在步骤 3 中生成身份验证令牌和密码,这就是您继续请求数据所需的全部。您只需要用户授权一次,并针对用户帐户/某种商店存储第 3 步授权 token/secret。
另请注意,您可以调用通知 - 新权重会触发您的网站自动刷新数据。如果您只想登录并查看最新数据而无需手动触发数据刷新/导致进一步延迟,那就太好了。
请注意 API 有过滤选项 - 因此请务必仔细阅读这些选项。
下面是一些可能有用的(基本)代码:
public async Task<string> GetData_BodyMeasures()
{
string rawdata = "";
try
{
string random = GetRandomString();
string timestamp = GetTimestamp();
string baseSignature = GetDataSignature_BodyMeasure(random, timestamp);
string hashSignature = ComputeHash(baseSignature, CONSUMER_SECRET, ACCESS_OAUTH_TOKEN_SECRET);
string codeSignature = UrlEncode(hashSignature);
string requestUrl = GetData_BodyMeasure_Url(codeSignature, random, timestamp);
HttpResponseMessage response = await client.GetAsync(requestUrl);
string responseBodyAsText = await response.Content.ReadAsStringAsync();
rawdata = responseBodyAsText;
}
catch (Exception ex)
{
}
return rawdata;
}
private string GetDataSignature_BodyMeasure(string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature_method", SIGNATURE_METHOD},
{ "oauth_timestamp", timestamp},
{ "oauth_token", ACCESS_OAUTH_TOKEN },
{ "oauth_version", AUTH_VERSION},
{ "userid", USER_ID }
};
StringBuilder sb = new StringBuilder();
sb.Append("GET&" + UrlEncode(BASE_URL_REQUEST_BODY_MEASURE) + "&action%3Dgetmeas");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append(UrlEncode("&"));
sb.Append(UrlEncode(urlItem.Key + "=" + urlItem.Value));
}
return sb.ToString();
}
private string GetData_BodyMeasure_Url(string signature, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "action", "getmeas"},
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature", signature },
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", ACCESS_OAUTH_TOKEN },
{ "oauth_version", AUTH_VERSION},
{ "userid", USER_ID }
};
StringBuilder sb = new StringBuilder();
sb.Append(BASE_URL_REQUEST_BODY_MEASURE + "?");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append("&");
sb.Append(urlItem.Key + "=" + urlItem.Value);
}
return sb.ToString();
}
我正在尝试通过 oauth(.net 核心 C#)访问我的 Withings/Nokia 缩放数据。
说明可在以下位置找到: https://oauth.withings.com/en/api/oauthguide
和API指南在这里: https://developer.health.nokia.com/api#step1
我已完成第 1 部分 - 我获得了授权令牌和密码。
第 2 部分 - 我已手动检索到授权我的应用程序使用我的 Withings 秤数据的代码 - 即作为回调结果的授权代码(通过 API 开发人员页面)。我假设这只需要完成一次就可以永久授权我的应用程序访问。我已将此值硬编码到我的代码中,并在我重新授权应用程序时更新它。
现在我卡在第 3 部分 - 获取访问权限 token/secret。 错误 = 无效签名
(使用上面的页面,我已经能够检索到我 4 年的体重秤数据,所以我知道它应该有效)。
我的基本签名与上述 API 测试页相同(随机数、签名和时间戳除外)。
我的 url 与上面的 API 测试页相同(除了 nonce 和时间戳)。
我不解的是为什么这适用于第 1 部分而不适用于第 3 部分。 是代码有问题还是仅仅是请求令牌在发出请求之前必须针对 application/users 数据进行授权? 不过肯定不用每次都跟用户重新授权吧??
我最初搞砸了第 1 部分并给出了无效签名错误 - 这显然是签名的问题 - 但我重新检查了第 3 部分中的签名并且它很好。
private const string AUTH_VERSION = "1.0";
private const string SIGNATURE_METHOD = "HMAC-SHA1";
private const string BASE_URL_REQUEST_AUTH_TOKEN = "https://developer.health.nokia.com/account/request_token";
private const string BASE_URL_REQUEST_ACCESS_TOKEN = "https://developer.health.nokia.com/account/access_token";
...
Withings w = new Withings();
OAuthToken t = await w.GetOAuthToken();
string token = t.OAuth_Token;
string secret = t.OAuth_Token_Secret;
OAuthAccessToken at = await w.GetOAuthAccess(t);
string aToken = at.OAuth_Token;
string aTokenSecret = at.OAuth_Token_Secret;
...
public async Task<OAuthAccessToken> GetOAuthAccess(OAuthToken authToken)
{
OAuthAccessToken token = new OAuthAccessToken();
try
{
string random = GetRandomString();
string timestamp = GetTimestamp();
string baseSignature = GetOAuthAccessSignature(authToken, random, timestamp);
string hashSignature = ComputeHash(baseSignature, CONSUMER_SECRET, authToken.OAuth_Token_Secret);
string codeSignature = UrlEncode(hashSignature);
string requestUrl = GetOAuthAccessUrl(authToken, codeSignature, random, timestamp);
HttpResponseMessage response = await client.GetAsync(requestUrl);
string responseBodyAsText = await response.Content.ReadAsStringAsync();
string[] parameters = responseBodyAsText.Split('&');
token.OAuth_Token = parameters[0].Split('=')[1].ToString();
token.OAuth_Token_Secret = parameters[1].Split('=')[1].ToString();
}
catch (Exception ex)
{
}
return token;
}
private string GetOAuthAccessSignature(OAuthToken authToken, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
//{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", END_USER_AUTHORISATION_REQUEST_TOKEN },
{ "oauth_version", AUTH_VERSION}
};
StringBuilder sb = new StringBuilder();
sb.Append("GET&" + UrlEncode(BASE_URL_REQUEST_ACCESS_TOKEN) + "&oauth_consumer_key%3D" + CONSUMER_KEY);
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append(UrlEncode("&"));
sb.Append(UrlEncode(urlItem.Key + "=" + urlItem.Value));
}
return sb.ToString();
}
private string GetOAuthAccessUrl(OAuthToken authToken, string signature, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature", signature },
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", END_USER_AUTHORISATION_REQUEST_TOKEN },
{ "oauth_version", AUTH_VERSION}
};
StringBuilder sb = new StringBuilder();
sb.Append(BASE_URL_REQUEST_ACCESS_TOKEN + "?");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count > 1) sb.Append("&");
sb.Append(urlItem.Key + "=" + urlItem.Value);
}
return sb.ToString();
}
备注:
- 我已经订购了我的参数
- 我有一个不错的url编码(纠正我错误)
- 我有一个 hmac-sha1 散列(纠正我错误)
- 对使用开放库不感兴趣 - 我想在没有第三方工具的情况下修复此代码
以下是我正在使用的辅助方法:
private string ComputeHash(string data, string consumerSecret, string tokenSecret = null)
{
// Construct secret key based on consumer key (and optionally include token secret)
string secretKey = consumerSecret + "&";
if (tokenSecret != null) secretKey += tokenSecret;
// Initialise with secret key
System.Security.Cryptography.HMACSHA1 hmacsha = new System.Security.Cryptography.HMACSHA1(Encoding.ASCII.GetBytes(secretKey));
hmacsha.Initialize();
// Convert data into byte array
byte[] dataBuffer = Encoding.ASCII.GetBytes(data);
// Computer hash of data byte array
byte[] hashBytes = hmacsha.ComputeHash(dataBuffer);
// Return the base 64 of the result
return Convert.ToBase64String(hashBytes);
}
// Get random string
private string GetRandomString()
{
return Guid.NewGuid().ToString().Replace("-", "");
}
// Get timestamp
private string GetTimestamp()
{
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds).ToString();
}
// Url Encode (as Uri.Escape is reported to be not appropriate for this purpose)
protected string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
protected string UrlEncode(string value)
{
var result = new StringBuilder();
foreach (var symbol in value)
{
if (UnreservedChars.IndexOf(symbol) != -1)
result.Append(symbol);
else
result.Append('%' + $"{(int)symbol:X2}");
}
return result.ToString();
}
谢谢,丹。
已解决 - 这是我对 oauth 工作原理的理解的问题。
第 1 步 - 获取令牌(这允许您根据您的 api 帐户申请提出请求)
第 2 步 - 创建一个 URL(使用上面的 2 分钟令牌)重定向用户以授权您的 Withings api 应用程序使用特定用户的帐户。当您传递它时返回相同的令牌 - 但现在将允许它在步骤 3 中发出请求。
第 3 步 - 请求访问令牌 - 这将为您提供一个令牌和秘密字符串,允许您继续访问此用户的帐户(对于您的 api 帐户申请)。
第 4 步 - 请求数据 - 方法与前面所有步骤类似 - 非常简单。 Returns一大串数据。阅读 API 文档,您可以过滤 - 这就是我将要做的,因为我有大约 4/5 年的 'interesting' 体重数据。
我在执行第 1 步然后执行第 3 步时认为从第 2 步返回的代码(没有注意到它与输入的代码相同)可以存储并用于第 3 步而无需重新授权。
您实际上(以及我所做的)是按照 API 演示界面在步骤 3 中生成身份验证令牌和密码,这就是您继续请求数据所需的全部。您只需要用户授权一次,并针对用户帐户/某种商店存储第 3 步授权 token/secret。
另请注意,您可以调用通知 - 新权重会触发您的网站自动刷新数据。如果您只想登录并查看最新数据而无需手动触发数据刷新/导致进一步延迟,那就太好了。
请注意 API 有过滤选项 - 因此请务必仔细阅读这些选项。
下面是一些可能有用的(基本)代码:
public async Task<string> GetData_BodyMeasures()
{
string rawdata = "";
try
{
string random = GetRandomString();
string timestamp = GetTimestamp();
string baseSignature = GetDataSignature_BodyMeasure(random, timestamp);
string hashSignature = ComputeHash(baseSignature, CONSUMER_SECRET, ACCESS_OAUTH_TOKEN_SECRET);
string codeSignature = UrlEncode(hashSignature);
string requestUrl = GetData_BodyMeasure_Url(codeSignature, random, timestamp);
HttpResponseMessage response = await client.GetAsync(requestUrl);
string responseBodyAsText = await response.Content.ReadAsStringAsync();
rawdata = responseBodyAsText;
}
catch (Exception ex)
{
}
return rawdata;
}
private string GetDataSignature_BodyMeasure(string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature_method", SIGNATURE_METHOD},
{ "oauth_timestamp", timestamp},
{ "oauth_token", ACCESS_OAUTH_TOKEN },
{ "oauth_version", AUTH_VERSION},
{ "userid", USER_ID }
};
StringBuilder sb = new StringBuilder();
sb.Append("GET&" + UrlEncode(BASE_URL_REQUEST_BODY_MEASURE) + "&action%3Dgetmeas");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append(UrlEncode("&"));
sb.Append(UrlEncode(urlItem.Key + "=" + urlItem.Value));
}
return sb.ToString();
}
private string GetData_BodyMeasure_Url(string signature, string random, string timestamp)
{
var urlDict = new SortedDictionary<string, string>
{
{ "action", "getmeas"},
{ "oauth_consumer_key", CONSUMER_KEY},
{ "oauth_nonce", random},
{ "oauth_signature", signature },
{ "oauth_signature_method", UrlEncode(SIGNATURE_METHOD)},
{ "oauth_timestamp", timestamp},
{ "oauth_token", ACCESS_OAUTH_TOKEN },
{ "oauth_version", AUTH_VERSION},
{ "userid", USER_ID }
};
StringBuilder sb = new StringBuilder();
sb.Append(BASE_URL_REQUEST_BODY_MEASURE + "?");
int count = 0;
foreach (var urlItem in urlDict)
{
count++;
if (count >= 1) sb.Append("&");
sb.Append(urlItem.Key + "=" + urlItem.Value);
}
return sb.ToString();
}