Monday 31 October 2016

Authentication for Amazon Voice Services

In an effort to have my new Echo Dot announce incoming telephone calls, I would need to have it do push notifications. Sadly, this is not yet available on the echo platform. Happily, there is a workaround via custom skills that relies on leveraging Amazon Voice Services - essentially building parts of an echo in software.

To do so, it's necessary to obtain an access token to facilitate calls to the voice services api. This is not a straightforward process - and it's just a prerequisite for the push notifications themselves. I'm logging the process here for future reference!


In achieving this, I relied heavily on these resources;


The basic process for configuring authentication with Alexa Voice Services is as follows;
  • STEP 1: Set up an Alexa Voice Service Device
  • STEP 2: Retrieve the Authentication Code
  • STEP 3: Retrieve the Access Token
  • STEP 4: Refresh the Access Token



STEP 1: Set up an Alexa Voice Service Device

In the Amazon Developer Portal, I clicked on Get Started for  Alexa Voice Services. Then, Register a product of type 'Device'.

From here, I followed the 'Set up AVS device' potion of Miguel's post, with the exception that I made my 'Allowed return URLs' under the web settings tab a little more straightforward.

It actually doesn't matter what you set these values to, The origins entry doesn't seem to be used (for authentication generation anyway) and the return URL is called in response to some input and will generate an error in the page but as we shall see, we're only interested in the URL parameters Amazon sends back so a simple https URL will suffice;



Notes: Oddly, the developer portal does not allow the deletion of devices. I ended up with three before I finally got things working.


STEP 2: Retrieve the Authentication Code

Again following Miguel's post, I created a bash script to call amazon and retrieve an authentication code linked to my developer account and the device I just set up. Here's the script I ended up with (private codes obfuscated);

CLIENT_ID="amzn1.application-oa2-client.d5zzzd10xxxxx4ffcb3f1fyyyyy642d"
DEVICE_TYPE_ID="my_device"
DEVICE_SERIAL_NUMBER=123
REDIRECT_URI="https://localhost"
RESPONSE_TYPE="code"
SCOPE="alexa:all"
SCOPE_DATA="{\"alexa:all\": {\"productID\": \"my_device\",\"productInstanceAttributes\": {\"deviceSerialNumber\": \"123\"}}}"
function urlencode() {
  perl -MURI::Escape -ne 'chomp;print uri_escape($_),"\n"'
}

AUTH_URL="https://www.amazon.com/ap/oa?client_id=${CLIENT_ID}&scope=$(echo $SCOPE | urlencode)&scope_data=$(echo $SCOPE_DATA | urlencode)&response_type=${RESPONSE_TYPE}&redirect_uri=$(echo $REDIRECT_URI | urlencode)"


echo ${AUTH_URL} > test.txt



 This differs slightly from the example I was working with;
  • I hard-coded the productID and deviceSerialNumber items in the SCOPE_DATA line as I just couldn't get the code to run using variables.
  • I saved the resulting URL to a text file rather than calling it as I was running this on a linux command line without a GUI.

I took the resultant URL from the text file and pasted it into a browser. It looked something like this;

https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.d56zzzzzbac4xxxxf091yyyyd&scope=alexa%3Aall&scope_data=%7B%22alexa%3Aall%22%3A%20%7B%22productID%22%3A%20%22my_device%22%2C%22productInstanceAttributes%22%3A%20%7B%22deviceSerialNumber%22%3A%20%22123%22%7D%7D%7D&response_type=code&redirect_uri=https%3A%2F%2Flocalhost

On accessing this URL, I needed to log in to Amazon and confirm I was seeking an authorization code. This was returned via a call to https://localhost which, as mentioned, showed an error page but the important part was the code parameter in the URL.

Notes: This was the most difficult step for me and the cause of requiring 3x device setups as I spent a good deal of time troubleshooting an apparent brick wall whereby Amazon reported an unknown issue that they promised they were working on. Adding a new device and re-setting the security profile seemed to resolve it.


STEP 3: Retrieve the Access Token

Armed with an authentication code, I could now try to retrieve the required access token.

Rather than run another bash script as per the reference examples, I chose to do this bit using the Chrome Postman addon as I just find it easier to work with.

I set it up to post url encoded form fields to https://api.amazon.com/auth/o2/token. These included grant_type, client_id, client_secret and redirect_uri keys with data as per the examples and my device security profile as well as the all-important code obtained in step 2.

Here's the set-up (again with pertinent data obfuscated);



Following a few tries where I had to fix obvious mistakes, I got back this;

{
  "access_token": "Atza|IwEBIEj4femu_gI0zzzzzzzzyJzcr0B26rs4PNxxxxxxxxxoYm3bwu1UzEwtoTAAt_Mp4mVo2scoD1yyyyyyyyyypGaayvPWwBOMOH_d-CHQKrfLexFN-6P6LrN7wp-HUW5-SbfLRI-LzeJvh2MlxfDk6ME84Xc9X_AN5jvoxkihVBRS16LVklwOTpEWON74gbezUnyf89d0hYz1s9usASUhg4rxICnvVKkDdoeeR4X0TA8bGT_KV3zbAwV99tHHappJcIyx_fHaBuGAdTiAFBtuu-9aRLyOjDmv64YpIWEqRiJAQexO3ei0h86p2KybYB6X6a91yNjUCcWSUhN3upFyT2tS6d_CaIttCzEe0uDkUhqVJyBqb4MY6p2kghBobuPgXcpwEjXSeoFi4xo0tnk9xzSElesA_YK8AwnyNj0klfdMlbW8sYeit1gjuobGhiPCk1xv6P9lpqD-TMnO_3WDtdw4k-pf7TMVPlzJIr81NyV9luZlSD9d_FJ",
  "refresh_token": "Atzr|IwEBIFwwwwwwwwwwTkS5nXtoQ-H9IOCQ3q5oYnwsem2AYgV3GmTz4PIqgggggggggggyJH0so4kGuC7dDJWtMpi1u6Q2Il4pUiXJul19rWsbkjgOs5x5ziwnMwbwVHN2mQarpLo5-tztvPTenWtXKb8Qxi5733ETj5vkp-kE5dh62F9KOmb94T7vvvvvvvvvvvvx6YyA3MrGO1P7ROgdY-6OBhelay8zNA1Jt-GfmEcl2bTjAmPSMuDzjnnv7aDJRjbWonJgIkhL_FF_hndzYQpHhVYiNJZOBxxHjs49YcbjGbzpZDvtoflvbLQ8Tf6gIAusB3WYkWVBtNX6E1DDIF_u5WssYCMfD3ndTqhyj2Wbr7IBFT8V41y1B03stv_I9gMS2tX1t4YfD_IU9-IUVIUfcccccccccccimk_OQahQSqzmPkr2sgJi7e5weL3ciWa6xUts2OpYxqmcxxxxxxxxxcSzQglZdybTwmPm",
  "token_type": "bearer",
  "expires_in": 3600
}



That's the access token I need to start making calls to Alexa Voice Services. Yay!

Notes: It's not documented anywhere that I could find but the authentication token from step 2 seems to be time limited. Once I had everything set up for step 3, I was getting notification that the code was bad. I had to run step 2 again to generate a new code which then worked perfectly.


STEP 4: Refresh the Access Token

Unfortunately, that;s not the end of it. The access token received is only good for 1 hour (the expires_in:3600 entry in the return data). At that point, it needs to be refreshed for another hour.

I could just send the refresh request in advance of any queries but, thinking long term, I may wish to use this service across multiple functions or even technologies so I thought it would be a good idea to save it and auto-renew.

I had a mySQL docker installed on unRAID to be used for openHAB persistence services. In here, I set up a new simple table ('noderedvars') for Node-RED persistent variable storage. It's just fields named 'varname' and 'varval'. I set up two records, one for 'access_token' and one for 'refresh_token' and stored the relevant values in there (the strings are ~1,200 characters so I set them up as varchar(2048), just to be sure).

Then, I set up a Node-RED function, triggered every 58 mins, which retirves the values, makes a call to the token refresh URL, retrieves new values and stores them in the DB. In this way, any app or function with access to the database can retrieve a valid access token.

Here's the flow and some details on the nodes;


AVS Auth Renew
The initial inject node is set to trigger every 58 minutes and contains the following in the Topic field;

SELECT `noderedvars`.`varname`,`noderedvars`.`varval` FROM `openHAB`.`noderedvars` WHERE `noderedvars`.`varname` = 'access_token' OR  `noderedvars`.`varname` = 'refresh_token';


openHab nodevars
A mysql node that contains the databse configuration details. The node runs SQL received as a message topic.


Prep Request
A custom function to prepare the POST request to be sent to Amazon. Code as follows;

var str = msg.payload;



//parse out the current  refresh token retrieved from DB

var refreshtk = str[1].varval;



//Prep data for HTTP request

msg.topic = "";

msg.headers = {"Content-type" : "application/x-www-form-urlencoded"}

msg.payload = "grant_type=refresh_token&refresh_token="+refreshtk+"&client_id=amzn1.application-oa2-client.dxxxxxxxxxffcb3f1yyyyyy642d&client_secret=d1820ae897077006exxxxxxx073d3f5db9yyyyyyyy3249a05zz353c&redirect_uri=http://localhost"



return msg;




http request
Send the request to Amazon;



Split Response Values
The split node parses the returned data and sends a separate message for each element, four in all comprising access_token, refresh_token, expires_in and token_type.No configuration required on this node, it's automatic.


update auth token
A custom function that looks at each of the four messages from the split function and passes on those containing tokens as SQL UPDATE commands in the message topic. Non token elements are ignored;

var str = msg.payload; //check each message from split to see if it's a token //if yes, send to database if (msg.parts.key == "refresh_token"){   msg.topic = "UPDATE `openHAB`.`noderedvars` SET `noderedvars`.`varval` = '"+str+"' WHERE `noderedvars`.`varname` = 'refresh_token';";   return msg; } else { if (msg.parts.key == "access_token"){     msg.topic = "UPDATE `openHAB`.`noderedvars` SET `noderedvars`.`varval` = '"+str+"' WHERE `noderedvars`.`varname` = 'access_token';"; return msg;      } else { return null; }}

As earlier, a mysql node pushes the new token to the database giving a new access token, good for another hour.

Notes: This flow has hardly any error-checking. It will need to be improved significantly to be robust enough to leave running permanently.




1 comment:

Steffie-Cel said...
This comment has been removed by a blog administrator.