Account Linking with OATH2, Alexa, and Nest
- 12 minutes read - 2512 wordsAs an update to my previous post on creating an Alexa skill to control Nest thermostats, in which we got an account pin manually, and used this for making calls to the Nest API, I was able to use the account linking methodology in Alexa to allow any user to authenticate with their Nest account, and thereby control their thermostats without this tedious step and code changes. Here’s how to do it.
Note, I began doing this a few days ago. Two days ago, Alexa added Authorization code grant support, making it easier. I’ll start with how to use that, which removes several steps, and then go into detail on how I got around this with the implicit grant earlier, using some AWS products such as S3 for storing the client secret in a secured file, and Elastic Beanstalk for calling the Nest authorization grant code flow.
Setting up Alexa and Nest with Authorization Grant linking.
OATH2 provides for two primary types of account linking, implicit grant, in which no client secret is needed, and authorization grant, in which an authorization code is returned, and used in combination with the client secret to get an access token. Implicit grants expire after a predetermined amount of time, requiring re-authentication, whereas authorization grants provide a refresh token which can be stored and used after the token expires to get a new access token.
Start with the example Nest skill created in this blog post. Here, we are using the implicit grant, and manually entering it into the code. Building upon that, we’ll use Alexa’s support for promoting for sign in and storing the codes.
In the Alexa skill configuration (at developer.amazon.com), enable account linking in the “Publishing Information” step.
These values can all be found on the “product configuration” on developer.nest.com within your app.
Authorization URL is the auth url for the service you are authenticating with.
Client Id is the client or product id of the app in the other system, in this case the “ProductID” for the Nest app.
Domain list lists the allowed domains that can be loaded during the flow. You only need Nest.com here.
Scope is not needed as Nest specifies the scopes desired when configuring the app.
Redirect URL shows the url that authentication should redirect to after competing. This is needed for the Nest configuration. We’ll come back to this.
As discussed, we want the Auth Code Grant which allows for longer lived app usage with refresh tokens.
The Access Token URL is given in the Nest product configuration. This is the url that Alexa will call with the authentication code along with client secret to get the access token.
Client Secret is the private (do not share this!) secret key used to verify that the caller is allowed by the owner of the app. This is shown in the nest configuration page.
Nest uses a request body rather than header scheme, so select Credential in request body for the scheme.
Back on the Nest developer site, edit your product to use the redirect URI given on the Alexa page. Make sure the url is https and includes the vendorId which Alexa needs to verify the app.
How it Works
If you recall from the previous blog, we essentially did all of these steps ourselves via the command line and curl. Now, Alexa and Nest will handle the authentication for us, and we don’t need to handle storing the code in the code itself.
We’ll have to change the skill code a bit still to kick this off and use the code. When the skill is launched, if no auth token is found, the skill will return an account link card, allowing the user to link their account with nest through the Alexa app or on the web. Alexa doesn’t provide a spoken method of linking. When clicking this card, the user will be taken to the Authorization URL, in this case, https://home.nest.com/login/oauth2?client_id=d3befc13-97ca-4460-b0a1-536796b8fb62&state=STATE
Here, they will sign in, and Nest will redirect to the Redirect URI specified, in this case https://pitangui.amazon.com/spa/skill/account-linking-status.html?vendorId=M2BQ6MEJRL1K01&state=STATE&code=CODE
This Alexa page will receive the code and use it to call the Access Token URL, https://api.home.nest.com/oauth2/access_token, with the code, clientId, client secret, and grant_type. We did this exact call with curl earlier
curl -X POST “https://api.home.nest.com/oauth2/access_token” -d “code=CODE” -d “client_id=CLIENTID” -d “client_secret=CLIENTSECRET” -d “grant_type=authorization_code”
This request will give back the access_token and refresh_token, which Alexa will store for us on the device, and pass in to each skill intent where our code can read it and use it for calls to the API.
Update the Alexa Skill Code
Within our skill code, the access_token will be passed in via the
session.user.accessToken
If this is null, the user has either not authenticated or has deleted the account link, so we need to get a new one. In this case, we return the linkAccount card type and the Alexa app will handle the rest.
We also need to verify that the access_token is valid. Other APIs provide a method for token verification, but Nest relies on 401 response codes to other APIs to tell the app that the token isn’t valid. We’ll update the code to handle a 401 from our calls and return an accountLink card as well.
All code has been pushed to my github repo here: https://github.com/Tylopoda/alexa-nest/tree/master/src
First, let’s update the AlexaSkill.js library, which oddly doesn’t have a built in linkAccount card.
Update the Response object’s buildSpeechletResponse method to add support for returning a linkAccount Card.
Response.prototype = (function () {
var buildSpeechletResponse = function (options) {
var alexaResponse = {
outputSpeech: createSpeechObject(options.output),
shouldEndSession: options.shouldEndSession
};
if (options.reprompt) {
alexaResponse.reprompt = {
outputSpeech: createSpeechObject(options.reprompt)
};
}
if (options.cardTitle && options.cardContent) {
alexaResponse.card = {
type: “Simple”,
title: options.cardTitle,
content: options.cardContent
};
}
if (options.linkAccount) {
alexaResponse.card = {
type: “LinkAccount”,
};
}
var returnResult = {
version: ‘1.0’,
response: alexaResponse
};
if (options.session && options.session.attributes) {
returnResult.sessionAttributes = options.session.attributes;
}
return returnResult;
};
We also need to add a method for the linkAccount card in the Response object
tellWithLinkAccount: function (speechOutput) {
this._context.succeed(buildSpeechletResponse({
session: this._session,
output: speechOutput,
linkAccount: true,
shouldEndSession: true
Now, in the main index.js for the skill, we need to verify the presence and validity of the token.
“StatusIntent”: function (intent, session, response) {
if(!session.user.accessToken) {
response.tellWithLinkAccount(“You must have a Nest account to use this skill. Please use the Alexa app to link your Amazon account with your Nest Account.”);
} else {
getNestFromServer(session.user.accessToken, function(body) {
console.log(“Status onResponse from nest: ” + body);
var bod = JSON.parse(body);
var responseString = “”;
for (i in bod) {
console.log(i);
var val = bod[i];
console.log(val.name);console.log(“name”, val.name);
console.log(“target temp”, val.target_temperature_f);
console.log(“current temp”, val.ambient_temperature_f);responseString += “The temperature ” + val.name + “ is ” + val.ambient_temperature_f + “. The temperature is set to ” + val.target_temperature_f + “. ”;
}response.tellWithCard(responseString, “Greeter”, responseString);
}, function() {
response.tellWithLinkAccount(“You must have a Nest account to use this skill. Please use the Alexa app to link your Amazon account with your Nest Account.”);
});
}},
Here, we just check if the token exists for the status intent. If not, we present the linkAccount card. I’ve also added a second callback function to be called in the event of a 401, unauthorized response code, which means our token isn’t valid. This will force a re-authentication within the app. We’re also now passing the token to the api calls.
Similarly, update the setTempIntent code which is a little trickier
“SetTempIntent”: function (intent, session, response) {
** if(!session.user.accessToken) {
response.tellWithLinkAccount(“You must have a Nest account to use this skill. Please use the Alexa app to link your Amazon account with your Nest Account.”);**
} else {var temperatureSlot = intent.slots.temperature;
var temperature = 0;
if (temperatureSlot && temperatureSlot.value) {
temperature = temperatureSlot.value;
}var thermostatSlot = intent.slots.thermostat;
var thermostat = “”;
if (thermostatSlot && thermostatSlot.value) {
thermostat = thermostatSlot.value;
}setNestTemperatureFromServer(thermostat, session.user.accessToken, function(body) {
console.log(“SetTemp onResponse from nest: ” + body);setNestTemperatureOnDeviceFromServer(body.device_id, temperature, session.user.accessToken, function(body) {
console.log(“SetTempDevice onResponse from nest: ” + body);response.tellWithCard(“Set Temperature ” + thermostat + “ to ” + temperature + “.”, “Greeter”, “Set temperature ” + thermostat + “ to ” + temperature + “.”);
}, function() {
response.tellWithLinkAccount(“You must have a Nest account to use this skill. Please use the Alexa app to link your Amazon account with your Nest Account.”);
});
}, function() {
response.tellWithLinkAccount(“You must have a Nest account to use this skill. Please use the Alexa app to link your Amazon account with your Nest Account.”);
});
}},
Again here we are checking both the existence of the token, and adding callbacks for invalid tokens to each api call. We’re also now passing the token to the api calls here.
To actually use the token and call our callback on 401 error, modify the functions.
function doRequest(options, eventCallback, requestNo, data, onUnAuthCallback) {
console.log(“calling ”, options.path);
if(requestNo > 5) {
console.log(“too many redirects”);
return;
}
var req = https.request(options, function(res) {
var body = “;
var redirect = false;
console.log(“statusCode: ”, res.statusCode);
console.log(“headers: ”, res.headers);
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers && res.headers.location) {
var location = res.headers.location;
console.log(‘redirect’, location);
redirect = true;
var redirectURI = url.parse(location);
console.log(‘redirect URI’, redirectURI);
options.hostname = redirectURI.hostname;
options.port = redirectURI.port;
options.path = redirectURI.pathname;
doRequest(options, eventCallback, requestNo + 1, data, onUnAuthCallback);
} else if (res.statusCode === 401) {
redirect = true;
if(req._auth) {
var authHeader = req._auth.onResponse(res);
if (authHeader) {
req.setHeader(‘authorization’, authHeader);
var location = res.headers.location;
console.log(‘redirect’, location);
var redirectURI = new URI(location);
console.log(‘redirect URI’, redirectURI);
options.hostname = redirectURI.hostname;
options.port = redirectURI.port;
options.path = redirectURI.pathname;
doRequest(options, eventCallback, requestNo + 1, data, onUnAuthCallback);
}
} else {
onUnAuthCallback();
}
}
res.on(‘data’, function(d) {
body += d;
});
res.on(’end’, function () {
if(body && !redirect) {
eventCallback(body);
} else {
console.log(‘redirectng so not done’);
}
});
});
if(data) {
req.write(data);
}
req.end();req.on(’error’, function(e) {
console.error(e);
});
}
In this method we’re just adding the call to the callback when the token is invalid.
And we modify each function for calling the Nest API to take the token and pass it in as follows
function getNestFromServer(nestToken, eventCallback, onUnAuthCallback) {
var options = {
hostname: ‘developer-api.nest.com’,
port: 443,
path: ’/devices/thermostats/’,
method: ‘GET’,
headers: {‘Authorization’ : ‘Bearer ’ + nestToken}
};doRequest(options, eventCallback, 0, null, onUnAuthCallback);
}
This simply replaces hardcoding the token in the code. Do this for each call.
Re-archive these files and upload to your AWS Lambda function and you should be all set!
I also added some logging to get the access_token sent in on launch. This helps for debugging.
console.log(“NestSkill onLaunch requestId: ” + launchRequest.requestId + “, sessionId: ” + session.sessionId + “, accessToken: ” + session.user.accessToken);
You can then use this in the lambdaFunction tester in AWS. If no access_token is set, you should receive a response with the linkAccount card, if a valid one is sent, you should get a successful response.
Fun with Authorization Codes
Before Alexa added the ability to use authorization codes, I had to do some creative coding to get the linking to work. With the implicit token, it was still possible to authenticate, but I needed to add an extra layer between the systems to handle the access token grant. To do this, I created a page on Elastic Beanstalk, my favorite AWS tool. Now the whole point was to take the hard coded private keys out of my code, so I needed a secure place to store it. I chose an encrypted, private S3 bucket with read access only to my elastic beanstalk instance. This way, in the code, I could read the value, use it for the access token, and call Nest’s api. This page then just redirects to the expected Alexa page with the access token and stores it. The code for the Alexa skill remains the same.
Setting up the S3 bucket and file
I created a normal S3 bucket. The bucket configuration doesn’t matter as much as the file, though you should ensure you keep the default permission which do not allow public access to the files. Giving public access would defeat the entire purpose of a secured location.
Create a properties file with the values you need. The clientID isn’t necessarily a secret, but I didn’t want to hard code it either.
clientid.properties
clientsecret=YOUR_SECRET_FOR_NEST
clientid=CLIENT_ID_FOR_NEST
Upload this to S3 through either the cli, the console, or explorer tool. Ensure it is configured without public access and encrypted.
Create a IAM policy for access to the file.
In the IAM tool in AWS, create a policy. Don’t use the preconfigured one, you’ll enter the policy definition manually.
{
“Version”: “2012-10-17”,
“Statement”: [
{
“Action”: [
“s3:GetObject”
],
“Sid”: “Stmt0123456789”,
“Resource”: [
“arn:aws:s3:::S3_BUCKET_NAME/clientid.properties”
],
“Effect”: “Allow”
}
]
}
Make sure the bucket name matches your S3 bucket name. Now, go to the roles tab and find the ec2 role. This is the role elastic beanstalk uses to invoke the server instance. Attach this new policy to it. You don’t need to restart your instance, it will automatically apply instantly.
Authorization code
Last we’ll set up the page to accept the parameters Nest passes, call the access token api, and redirect to the Alexa page with the code. Setting up an elastic beanstalk instance with spring is outside the scope of this, but AWS has excellent documentation on how to do so.
package springapp.web;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.client.fluent.Content;
import org.apache.http.client.fluent.Form;
import org.apache.http.client.fluent.Request;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.S3Object;
import com.fasterxml.jackson.databind.ObjectMapper;
@Controller
public final class AuthenticateNest {
@RequestMapping(“/authenticatenest”)
public String authenticatenest(
@RequestParam(“state”) final String state,
@RequestParam(“code”) final String code,
HttpServletResponse response) {
String accessToken = “”;
try {
String url = “https://api.home.nest.com/oauth2/access_token”;
AmazonS3Client s3Client = new AmazonS3Client();
Properties properties = loadProperties(s3Client);
String clientid = properties.getProperty(“clientid”);
String clientsecret = properties.getProperty(“clientsecret”);
Content c = Request.Post(url)
.bodyForm(Form.form().add(“code”, code)
.add(“client_id”, clientid)
.add(“client_secret”, clientsecret)
.add(“grant_type”, “authorization_code”)
.build())
.execute()
.returnContent();
Map m = new ObjectMapper().readValue(c.toString(), Map.class);
accessToken = (String) m.get(“access_token”);
Integer expires_in = (Integer) m.get(“expires_in”);
} catch (Exception e) {
System.out.println(“ERROR: ” + e);
}
String redirecturl = “https://pitangui.amazon.com/spa/skill/account-linking-status.html?”;
redirecturl += “vendorId=M2BQ6MEJRL1K01”;
redirecturl += “#state=” + state;
redirecturl += “&token_type=Bearer”;
redirecturl += “&access_token=” + accessToken;
return “redirect:” + redirecturl;
}
private static final String BUCKET = “S3_BUCKET”;
private static final String PROPERTIES_KEY = “clientid.properties”;
private static Properties loadProperties(AmazonS3Client client) throws IOException
{
S3Object propertiesObject = client.getObject(BUCKET, PROPERTIES_KEY);
Properties properties = new Properties();
properties.load(propertiesObject.getObjectContent());
return properties;
}
}
Set your S3 bucket as well containing the properties file. This code expects a state and code parameter from NEST. The state is passed directly through from Alexa, and needs to be returned as is to verify the flow. The code is the temporary authentication token. We use this, along with the client id and secret, stored in our property file, to get an access token. We then redirect to the Alexa url with state, vendorId, token type, and the access token populated from the call.
Publish this to your Elastic Beanstalk and configure Nest to redirect to this page instead of Alexa. You’re now set to use implicit tokens.