OAuth2 Hack Using only openHAB. Good idea?

Just to play around I was looking into what it would take to interact with Dexcom’s API. The API is well documented and has a nice sandbox area to experiment in, BUT, it requires OAuth2.

I know the correct approach is to deploy my own instance of openHAB cloud and set it up with the redirect URL able to process the authtoken and retrive the access token. But I think there is a way I can do this using openHAB itself and myopenhab.org. I’m just playing here and know this is probably a bad idea. But I’ve not a lot of experience with this.

How bad of an idea is this?

Here is my approach:

  1. Configure the redirect URL to https://myopenhab.org/static/authcode.html.

  2. authcode.html is a static page in conf/html with some javascript to parse out the authcode passed to me as a URL parameter and issue an XMLHttpRequest to the OH REST API. I’ve implemented up to this point and it works.

  3. Trigger a Rule with the authcode and make the appropriate HTTP POST back to Dexcom to get another callback with the acess token, refresh token, and time to live of the access token. the auth code is only valid for one minute.

  4. Create another static html page to catch the return call and retrieve the access token, refresh token, and amount of time it will live and post those to Items using XMLHttpRequest again.

  5. In some Rules I’ll make the REST calls to query the API for the data I want using the access token and watching for the end of the time to live for the access token and/or watching for a return code of 401 and then using the refresh token to get a new access token and refresh token.

Like I mentioned, I’ve already implemented this up to getting the authcode and POSTing it to an Item and I know how to do the rest. I’m pretty sure it will work. But it feels sketchy as heck. What are people’s thoughts? Anyone have minor suggestions?

I already know, the proper way would be to set up my own openHAB Cloud instance. But where’s the fun in that? :slight_smile:

Well that didn’t work. I missed the part where the the response sent in step 4 that has the auth and refresh tokens gets sent as a post to the redirect url you pass to it.

I’ll experiment with whether I can make it use an OH REST API call to POST to an Item but I suspect I’m dead in the water.

1 Like

It’s strange that dexcom sends the token by issuing a post-request to the client, that would mean the client need to run a webserver listening to that address. The OAuth2 specification states that the access token should be provided in the http-response of the request the client sends in step 3 above, so they are not following the spec in that case. Might be worth pointing that out to them?

https://tools.ietf.org/html/rfc6749#section-5.1

I have connected to OAuth2-protected apis using a similar method, (but using external scripts and some manual interaction) but have never needed to catch a post-request from the authorization server…

Doh! I spent the past 30 minutes trying compile everything I’ve tried and the results I get back only to realize that I just did something stupid. It is working as it should. I was looking at the Message, not the Response. I’m back in business.

Though, since you’ve done something like this before, do you know how to avoid the COR error when I try to make the XMLHttpRequest back to dexcom from my html page instead of the Rule? It would be nice to do the full round trip in the HTML page rather than needing a Rule. But I do have it working in the Rule now.

I will post a tutorial once I get it working all the way.

Thanks for the push in the right direction. I always assume I’m doing something wrong and I’m usually right. :wink:

For the curious, the WIP is as follows:

I use http://myopenhab.org/static/test.html as the redirect URL registered with Dexcom.

The code below is pretty sloppy and a WIP.

Here is test.html (I just copied the JS that parses out the arguments from a posting somewhere and haven’t reviewed it). I bet it can be done better.

<!DOCTYPE html>
<html>
        <head>
                <title>OAuth2 Catcher</title>
        <script type="text/javascript">
        function getAllUrlParams(url) {

  // get query string from url (optional) or window
  var queryString = url ? url.split('?')[1] : window.location.search.slice(1);

  // we'll store the parameters here
  var obj = {};

  // if query string exists
  if (queryString) {

    // stuff after # is not part of query string, so get rid of it
    queryString = queryString.split('#')[0];

    // split our query string into its component parts
    var arr = queryString.split('&');

    for (var i=0; i<arr.length; i++) {
      // separate the keys and the values
      var a = arr[i].split('=');

      // in case params look like: list[]=thing1&list[]=thing2
      var paramNum = undefined;
      var paramName = a[0].replace(/\[\d*\]/, function(v) {
        paramNum = v.slice(1,-1);
        return '';
      });

      // set parameter value (use 'true' if empty)
      var paramValue = typeof(a[1])==='undefined' ? true : a[1];

      // (optional) keep case consistent
      paramName = paramName.toLowerCase();
      paramValue = paramValue.toLowerCase();

      // if parameter name already exists
      if (obj[paramName]) {
        // convert value to array (if still string)
        if (typeof obj[paramName] === 'string') {
          obj[paramName] = [obj[paramName]];
        }
        // if no array index number specified...
        if (typeof paramNum === 'undefined') {
          // put the value on the end of the array
          obj[paramName].push(paramValue);
        }
        // if array index number specified...
        else {
          // put the value at that index number
          obj[paramName][paramNum] = paramValue;
        }
      }
      // if param name doesn't exist yet, set it
      else {
        obj[paramName] = paramValue;
      }
    }
  }

  return obj;
}
var code = getAllUrlParams().code;
        </script>
        </head>
<script>
document.write("Preparing to update AuthCode Item...")
var code = getAllUrlParams().code;
var xhr = new XMLHttpRequest();
xhr.open('POST', "https://myopenhab.org/rest/items/AuthCode");
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.setRequestHeader("Accept", "application/json");
xhr.onreadystatechange = function() {
        if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
                document.write("Received  " + code);
        }
        else if(xhr.status != 200) {
                document.write("Error in callback: " + xhr.status + " " + xhr.responseText);
        }
}
document.write("Sent " + code + "...");
xhr.send(code)
</script>

</html>

And the Rule:

import org.eclipse.xtext.xbase.lib.Functions
import java.net.URL
import java.nio.charset.StandardCharsets
import javax.net.ssl.HttpsURLConnection
import com.sun.org.apache.xml.internal.security.utils.Base64
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Date

val log = "dexcom"
val clientId = "CLIENT ID"
val clientSecret = "CLIENT SECRET"
val redirect = "https://myopenhab.org/static/test.html"

val tokenEndpoint = "https://sandbox-api.dexcom.com/v2/oauth2/token"

rule "Received authcode"
when
    Item AuthCode received command
then
  logInfo(log, "Starting to process authcode...")
  val code = receivedCommand.toString
  logInfo(log, "Received authcode: " + code)
  val tokenURL = new URL(tokenEndpoint)
  val HttpsURLConnection connection = tokenURL.openConnection() as HttpsURLConnection
  val request = 'client_secret='+clientSecret+'&client_id='+clientId+'&code='+code+'&grant_type=authorization_code&redirect_uri='+redirect
  logInfo(log, request)
  connection.setRequestProperty('content-type', 'application/x-www-form-urlencoded')
  connection.setRequestProperty('cache-control', 'no-cache')
  connection.requestMethod = "POST"

  connection.doOutput = true
  connection.setDoInput = true

  connection.outputStream.write(request.getBytes("UTF-8"))

  val responseCode = connection.responseCode

  logInfo(log, "HTTP Code: " + responseCode)
  logInfo(log, "Message: " + connection.responseMessage)

  val StringBuffer sb = new StringBuffer()
  val inputStream = new BufferedInputStream(connection.getInputStream())
  val BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))
  var String inputLine = ""

  while ((inputLine = br.readLine()) != null) {
    sb.append(inputLine)
  }

  logInfo(log, "Response: " + sb.toString)
end

rule "Received tokens"
when
  Item Tokens received command
then
  logInfo(log, receviedCommmand.toString)
end
``

Note, this was mostly shamelessly copied from https://community.openhab.org/t/icloud-device-data-integration-in-openhab/32329.

I haven’t tried to automate that part at all. What I did is putting a html-file in my conf/html-folder that just displays the authoraization code returned by the callback. I then copy-paste it into a command-line script (written in ruby) that takes care of obtaining the access token and saving it to a file. So except for using the html-folder I leave OpenHAB completely out of the loop.

I like your idea of posting the auth-code directly to the rest api though, might try that some other time! But, as you say, doing everything by javascript is probably impossible due to CORS…

Edit: Just a side-note: shouldn’t you mask your client id and secret in the rule above? :wink:

Cool. My original ideas were to use your type of approach (though I probably would have used Python). I only have a minute from getting the authcode to make the call to get the tokens before the authcode expires. Since I wanted to make this something that less technical people might be able to use it was important to me that it be automated.

And since I need to get the authtoken again periodically I suppose it makes sense to write this in Rules anyway (OK, so it really makes sense to write it as a binding).

It looks like I’m back in business. The biggest hurdle I see now is convincing my wife to let me link up her account for testing. :slight_smile:

Shoot, I thought I had. I edited the posting so many times I guess I forgot. I’ll regenerate new ones as well. Thanks for noticing.

I actually only needed to authorize one time, then I can always use the refresh token to get a new token when it expires (hence my script is really terrible…). Might be different from service to service though.

No problem!

Oh, this works the same. I get a refresh token that I use to make the call to get a new auth token and refresh token. What I was referring to was I would have to implement the HTTP connection stuff in my Rules anyway so I wasn’t really saving myself any work by trying to get the tokens using XMLHttpRequest. Even though I can do the request in like 8 lines of code in the JS and needed a page’s worth of code in Java/Rules DSL.

For that reason I have moved all my complex rules that interacts with web services to external scripts (communicating with them via mqtt), the rules get very clunky very fast when trying to do such things…

Especially when you need to get much data from a json-response I find this much easier. In the rules you need to use the jsonpath transform for every value, which means openhab needs to parse the same json multiple times, extracting just one value each time. Feels like a waste of resources. Would be useful if you could parse a json string into a map directly in the rules instead.

I think there has been an open issue on ESH for while to make a JSON parser available in Rules.

For this I think I need to stick to Rules as my intent is to provide a way for others to easily make it work for them (my wife has zero interest in tying her BS numbers to the home automation) and requiring external scripts opens up a whole realm of problems I don’t want to have to help people solve. Exec and executeCommandLine problems are my least favorite prolbems to help people with.

Were I doing this only for myself, I’d probably write a module for https://github.com/rkoshak/sensorReporter and do it all in Python, or use JSR223 and again, do it all in Python or JavaScript. Ultimately, I’d like to write it up as a binding as I think Dexcom might be interested in this sort of unique use case. But I don’t know how that can work with the client ID and secret with an opensource project.

Maybe I’ll write it up and submit it to Mission to Make or Hack a Day or something at some point. Maybe the folks over on NightWatch or NightScout would be interested in something like this. Or maybe no one cares, but it’s fun to figure this sort of thing out.

I see what you mean, if I had planned on writing a tutorial I would probably have done it in a different way.

Have been thinking of trying out making a binding myself, but from what I read here in the forum it’s not quite clear how to implement OAuth? I know some bindings have done it, but haven’t studied how they have implemented it.

I know that for IFTT, Alexa, and Google Assistant the OAuth is implemented in myopenhab.org and that is I think the official position on where the OAuth stuff belongs. Other services like Google and Nest provide other approaches that seem to work by downloading or copying the auth code or the like. I don’t know everything, but I don’t know of any bindings that implement the OAuth stuff directly. I’m not sure how it could as what redirect url would it use?

There’s also a Spotify connect binding that uses OAuth, there the user needs to register a Dev account to get their own client is and secret. Then the binding seems to host its own servlet to handle authorization.

1 Like

Well shoot. I get it almost all the way working only to discover the API delays access to the data for 3.5 hours. That makes it pretty much useless in this context. Oh well, at least I figured out the OAuth2 stuff. I’ll post a tutorial on that I guess.

Wow, that’s just useless. Who wants data from 3.5 hours ago? On the upside, you have a working way to get OAuth authorization, might be useful for something else :smile:

Apparently is has to do with the FDA. They were concerned people might be writing their own non-FDA approved apps to provide treatment recommendations. So Dexcom delays the data by 3.5 hours to prevent that from happening. It certainly reduces the use of the data and the API.

I may look into other sources but my wife has no interest in this and she’s the one with the Dexcom so I will probably focus my efforts elsewhere.

For what it’s worth, I’ve posted an OAuth2 tutorial so, like you say, the effort wasn’t completely wasted.