Search Unity

How do I request an oauth token in FitBit with WWW

Discussion in 'Editor & General Support' started by fkberthold, Jul 20, 2014.

  1. fkberthold

    fkberthold

    Joined:
    Jul 2, 2014
    Posts:
    5
    This is a repost from answers. But since I got dead air there, I'm hoping for a bit more luck here.

    I am trying to connect to FitBit using the WWW class in C#. I used Let's Tweet in Unity as a starting point, although I've ripped most of it out. When I try to get an oauth token, it fails with a "401 Unauthorized" message. I've tried directly connecting via curl with, as far as I can tell, the same headers and it's successful. Can anyone spot what I'm doing wrong?

    When I enter this at the command line:

    curl -i -X 'POST' 'https://api.fitbit.com/oauth/request_token' -H 'Authorization: OAuth oauth_consumer_key="9e2fe319f5154a658731f73ce8373ef6", oauth_nonce="47F7793F", oauth_signature="u4Mvy%2BfR5rk8C%2BXERSz%2Bc%2F9ppj0%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1404279962", oauth_version="1.0"'​
    I Get:
    HTTP/1.1 200 OK
    Server: nginx
    Date: Wed, 02 Jul 2014 05:47:57 GMT
    Content-Type: application/x-www-form-urlencoded;charset=UTF-8
    Content-Length: 126
    Connection: keep-alive
    X-UA-Compatible: IE=edge,chrome=1
    Set-Cookie: fhttps=""; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/
    Expires: Thu, 01 Jan 1970 00:00:00 GMT
    Cache-control: no-store, no-cache, must-revalidate
    Pragma: no-cache
    Content-Language: en-US
    X-Frame-Options: SAMEORIGIN

    oauth_token=5c87cb5625ad6afdac957c7460d5c1d3&oauth_token_secret=73960a8e22f3c7c2cfb258fd8f83221e&oauth_callback_confirmed=true% ​


    Using this code in Unity:

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System;
    4. using System.Collections;
    5. using System.Text;
    6. using System.Text.RegularExpressions;
    7.  
    8. public class Demo : MonoBehaviour
    9. {
    10.  
    11.    public class RequestTokenResponse
    12.    {
    13.      public string Token { get; set; }
    14.      public string TokenSecret { get; set; }
    15.    }
    16.    
    17.          // You need to register your game or application in Fitbit to get cosumer key and secret.
    18.          // Go to this page for registration: https://dev.fitbit.com/apps/new
    19.    public string CONSUMER_KEY = "9e2fe319f5154a658731f73ce8373ef6";
    20.    public string CONSUMER_SECRET = "75f4e6d41d5f4469a49e8b929dd0b6b4";
    21.    private string RequestTokenURL = "http://api.fitbit.com/oauth/request_token";
    22.  
    23.    // Use this for initialization
    24.    IEnumerator Start()
    25.    {
    26.         byte[] dummy = new byte[1];
    27.         dummy[0] = 0;
    28.    
    29.         String timestamp = "1404279962";
    30.         String signature = "u4Mvy+fR5rk8C+XERSz+c/9ppj0=";
    31.    
    32.         StringBuilder parameters = new StringBuilder();
    33.         parameters.AppendFormat("OAuth oauth_consumer_key=\"{0}\",oauth_nonce=\"{1}\",oauth_signature=\"{2}\"," +
    34.          "oauth_signature_method=\"{3}\",oauth_timestamp=\"{4}\",oauth_version=\"{5}\"",
    35.          "9e2fe319f5154a658731f73ce8373ef6",
    36.          "47F7793F",
    37.               UrlEncode(signature),
    38.          "HMAC-SHA1",
    39.           timestamp,
    40.           "1.0");
    41.    
    42.            Hashtable headers = new Hashtable();
    43.            headers["Authorization"] = parameters.ToString();
    44.            Debug.Log ("Authorization Header: " + headers["Authorization"]);
    45.    
    46.    
    47.            WWW web = new WWW(RequestTokenURL, dummy, headers);
    48.    
    49.            yield return web;
    50.    
    51.            if (!string.IsNullOrEmpty(web.error))
    52.            {
    53.              Debug.Log(string.Format("GetRequestToken - failed. error : {0}", web.error));
    54.            }
    55.            else
    56.            {
    57.              String Token = Regex.Match(web.text, @"oauth_token=([^&]+)").Groups[1].Value;
    58.              String TokenSecret = Regex.Match(web.text, @"oauth_token_secret=([^&]+)").Groups[1].Value;
    59.              Debug.Log (string.Format ("Token: {0} TokenSecret: {1}", Token, TokenSecret));
    60.            }
    61.          }
    62.        
    63.          private static string UrlEncode(string value)
    64.          {
    65.            if (string.IsNullOrEmpty(value))
    66.            {
    67.              return string.Empty;
    68.            }
    69.          
    70.            value = Uri.EscapeDataString(value);
    71.          
    72.            // UrlEncode escapes with lowercase characters (e.g. %2f) but oAuth needs %2F
    73.            value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());
    74.          
    75.            // these characters are not escaped by UrlEncode() but needed to be escaped
    76.            value = value
    77.              .Replace("(", "%28")
    78.                .Replace(")", "%29")
    79.                .Replace("$", "%24")
    80.                .Replace("!", "%21")
    81.                .Replace("*", "%2A")
    82.                .Replace("'", "%27");
    83.        
    84.            // these characters are escaped by UrlEncode() but will fail if unescaped!
    85.            value = value.Replace("%7E", "~");
    86.        
    87.            return value;
    88.          }
    89.      
    90.          // Update is called once per frame
    91.          void Update()
    92.       {
    93.          }
    94.       }
    95.  
    96.  

    When I execute it I get:

    Authorization Header: OAuth oauth_consumer_key="9e2fe319f5154a658731f73ce8373ef6",oauth_nonce="47F7793F",oauth_signature="u4Mvy%2BfR5rk8C%2BXERSz%2Bc%2F9ppj0%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1404279962",oauth_version="1.0"
    UnityEngine.Debug:Log(Object)
    <Start>c__Iterator0:MoveNext() (at Assets/Demo.cs:44)

    GetRequestToken - failed. error : 401 Unauthorized
    UnityEngine.Debug:Log(Object)
    <Start>c__Iterator0:MoveNext() (at Assets/Demo.cs:53)​


    Thanks for taking the time to look through my code.

    -Frank B
     
    Last edited: Jul 20, 2014
  2. Graham-Dunnett

    Graham-Dunnett

    Administrator

    Joined:
    Jun 2, 2009
    Posts:
    4,287
    I can't see anything wrong with this fixed code, so I suspect the problem has something to do with how the WWW request is made. The POST request that Unity makes includes the single byte data you have (dummy) and the headers include:
    Code (csharp):
    1. Content-Length: 1
    2. Content-Type: application/x-www-form-urlencoded
    So it's possible either of those are unexpected by fitbit and results in an error. Maybe get a working curl request, and then add additional headers and see if you can cause it to fail. When I use Curl and add the content length header, and an arbitrary data value, I get:
    Code (csharp):
    1. {"errors":[{"errorType":"oauth","fieldName":"oauth_signature","message":"Invalid signature: OpUReIjECP/Z7HI0ALhyiZUEF2s="}],"success":false}
    So, I'd assume the presence of the dummy data (which appends to the URL) changes the signature, so the computed signature on the server doesn't match the computed one from the client.
     
  3. fkberthold

    fkberthold

    Joined:
    Jul 2, 2014
    Posts:
    5
    Thanks Graham, that sounds reasonable and is what I'd suspected was causing my chaos. WWW doesn't seem to have a way to leave off the 'dummy' value, is there a lower level library that I can reasonably use to do the same task?

    -Frank
     
  4. Graham-Dunnett

    Graham-Dunnett

    Administrator

    Joined:
    Jun 2, 2009
    Posts:
    4,287
    There isn't a lower API, no. The byte array is necessary to trigger Unity to do a POST request.
     
  5. Ian-Dundore

    Ian-Dundore

    Unity Technologies

    Joined:
    Dec 14, 2012
    Posts:
    7
    Hi Frank,

    It seems the problem here is the combination of Unity's WWW API and the OAuth specification. Essentially, the value of the oauth_signature parameter in your authorization header needs to be recomputed.

    According to section 3.4.1.3.1 of the OAuth spec, the signature parameter's computation is dependent on all of its request parameters, and the request parameters include any form/body data attached to the request. Unity, of course, requires you to include a nonzero-length raw body with your request, which therefore changes the computation of the oauth_signature.

    This means you won't be able to rely on Fitbit's debug tool to generate oauth_signature parameters; instead, you'll have to compute the parameter at runtime.

    For deployment in an actual game/app, you'd have to do this computation anyway. The oauth_signature parameter's computation also depends on the remainder of the authorization header, which includes a UNIX timestamp - and the timestamp's value changes frequently.

    Twitter has a detailed guide to constructing an OAuth Signature parameter here; I find it more readable than the raw RFC specification. :)
     
    Graham-Dunnett likes this.
  6. scarletshark

    scarletshark

    Joined:
    Jun 6, 2013
    Posts:
    33
    I'm sure that there is a way to do this in Unity, but after discussing with a good friend, it seems to make much more sense to create a web server to handle this request, and forward it (without the unnecessary body) to FitBit's API on my behalf. I'm still generating the signature in Unity, which should be just as secure.

    So the concept is:
    • Compile all required parameters in C#, including generating a nonce and timestamp
    • Generate the signature in C# as defined in the oauth documentation
    • Send a GET request to your own web server, and include all of the required parameters to authenticate
    • Using the supplied parameters, create the Authorization Header in PHP
      • Again, don't forget to use urlencode on your signature
    • POST only the authorization header to fitbit's API

    Example (PHP not JavaScript):
    Code (JavaScript):
    1. <?php
    2.     if($_SERVER["REQUEST_METHOD"] == "GET"){
    3.         //retrieve request parameters sent from Unity
    4.         $oauth_consumer_key =       $_GET["oauth_consumer_key"];
    5.         $oauth_nonce =              $_GET["oauth_nonce"];
    6.         $oauth_signature =          $_GET["oauth_signature"];
    7.         $oauth_signature_method =   $_GET["oauth_signature_method"];
    8.         $oauth_timestamp =          $_GET["oauth_timestamp"];
    9.         $oauth_version =            $_GET["oauth_version"];
    10.         $dest = $_GET["dest"];
    11.  
    12.         //generate the Authorization string that will be supplied in header
    13.         $auth = "Oauth ";
    14.         $auth .= "oauth_consumer_key=\"" .      $oauth_consumer_key .           "\",";
    15.         $auth .= "oauth_nonce=\"" .             $oauth_nonce .                  "\",";
    16.         $auth .= "oauth_signature=\"" .         urlencode($oauth_signature) .   "\",";
    17.         $auth .= "oauth_signature_method=\"" .  $oauth_signature_method .       "\",";
    18.         $auth .= "oauth_timestamp=\"" .         $oauth_timestamp .              "\",";
    19.         $auth .= "oauth_version=\"" .           $oauth_version .                "\"";
    20.  
    21.         //cURL expects the headers to be an array of headers
    22.         //POST headers are written as "key: value"
    23.         $headers = array("Authorization: " . $auth);
    24.  
    25.         HttpPostHeader($dest, $headers);
    26.     }
    27.  
    28.     function HttpPostHeader($url, $headers) {
    29.         $curl = curl_init();
    30.  
    31.         curl_setopt($curl, CURLOPT_URL, $url);
    32.  
    33.         //POST should have a header.  Supply the header
    34.         curl_setopt($curl, CURLOPT_HEADER, true);
    35.         curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    36.  
    37.         //Turn off the body
    38.         curl_setopt($curl, CURLOPT_NOBODY, true);
    39.  
    40.         curl_setopt($curl, CURLOPT_POST, true);
    41.         curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    42.  
    43.         $response = curl_exec($curl);
    44.  
    45.         echo $response;
    46.      }
    47. ?>
     
    Last edited: Apr 2, 2015
  7. TEvashkevich

    TEvashkevich

    Joined:
    May 14, 2015
    Posts:
    9