Amazon has been slowly upping their game in this department. First, they added account linking in general, and then they added a more complete OAuth flow, which includes both an access and refresh token. This allows a user to link their account once and keep accessing the API until the tokens are revoked.

After a couple of hoops, I think I have it working. Here's what I did.

First, go to your Alexa Skill in the Amazon Dashboard and add the ability to link accounts. Select the "Auth Code Grant" option. Using your Connected App settings in Salesforce, fill out the "Client ID" in the first part with your public key, then use the private key for the "Client Secret" in the token endpoint information below. Set the Authorization URL to Salesforce's login. This is what mine looks like:

setup


Note: When I took that screenshot, it worked fine. Mysteriously, it stopped adding the redirect URI. If that happens to you can just add it to the Authorization URL (just append "?redirect_uri=https://pitangui.amazon.com.etc").

Now, you might notice something odd. In a perfect world, you could set the Token URI to be Salesforce's Token URI and pretty much just be done. However, there are two things in this imperfect world that will keep that from working:

  1. Amazon's request is a bit non-standard. It doesn't seem to include a Content-Type header, and it seems to be in JSON and not form-encoded. Salesforce's Token URI requires both.
  2. The Salesforce response is also a bit non-standard. There doesn't seem to be an expires_in attribute, and we also send over the instance URL because it is required for the API requests.

To solve this, I've added an endpoint to my existing demo to basically act as a middle man between 1 and 2. My long term goal will be to make what I'm doing here a proper node package that other Salesforce developers can adopt to easily use OAuth in their projects, but for the sake of expediency, here is endpoint using Express:

app.post('/token',function (req, res) {
    console.log(req.body);
    var sr = sync_request('POST', 'https://login.salesforce.com/services/oauth2/token',
        {
            headers: {'Content-Type':'application/x-www-form-urlencoded','Accept':'application/json'},
            body: 'grant_type='+req.body.grant_type+'&code='+req.body.code+'&refresh_token='+req.body.refresh_token+'&client_id='+req.body.client_id+'&client_secret='+req.body.client_secret+'&redirect_uri='+req.body.redirect_uri
        });
    console.log(sr.getBody('utf8'));
    response = JSON.parse(sr.getBody('utf8'));

    response.access_token = response.access_token + " " + response.instance_url;
    response.expires_in = 5400; //in seconds, set this to be less than your setting under session management.

    res.jsonp(response);
});


That endpoint is what I put into the token URI in the Amazon setup.

After that, you just split on the space to re-create the token and instance URL:

if(req.body.session == null || req.body.session.user == null || req.body.session.user.accessToken == null) {
    send_alexa_response(res, 'Please log into Salesforce', 'Salesforce', 'Not Logged In', 'Error: Not Logged In', true);
   }

   oauth = {
        access_token : req.body.session.user.accessToken.split(" ")[0],
        instance_url : req.body.session.user.accessToken.split(" ")[1]
        }


Amazon will send along the most recent access token (with our instance URL riding piggy-back). You won't see the refresh token because you don't need it. Amazon will request a new access token via the same endpoint in an interval as defined by expires_in. So in my case, it grabs one every 90 minutes to be sure it beats the two-hour session timeout.

One thing to note: I cannot log into Salesforce using the Alexa app. I don't know why, I'm trying to get a conversation with Amazon going about it. It does work from a desktop browser though (http://echo.amazon.com).

Keep your eyes here on a more mature version of this solution.