Alexa Custom OAuth Account Linking with Rails

Custom skills with Amazon Echo devices provide a great way for users to interact with your product or service by voice. This is something particularly true at Cookpad where users are cooking, so their hands are often busy or messy with ingredients. Voice provides an excellent way to interact with a recipe while cooking.

Conversely, searching by voice is really not a great experience. We’re all used to searching websites and perusing lists of results, quickly drilling down to something we want to cook. Try to do the same thing by voice and get the results as audio responses becomes laborious and even frustrating.

Ecosystem

An ideal scenario then would be to search, bookmark or even post our own recipes using the Cookpad app or website, but then being able to instantly access these recipes we already decided are of interest by voice on Alexa.

Enter account linking…

Account Linking Overview

Amazon provides a way to authenticate user accounts between third party services and Alexa using the industry standard OAuth strategy. What this means is as a Cookpad user you can log in to Cookpad and then an Authentication Token is generated that Amazon stores in your Alexa user account. Then when Alexa makes API calls to Cookpad for recipes, this token is sent along with the request, so Cookpad knows what user is interacting and can securely retrieve that user’s bookmarks and their own recipes they may have shared.

The simplest way to handle OAuth is to use Amazon’s own “Log in with Amazon” or LWA, but since it’s not widely used by third party services like Cookpad as a way for their users to sign in, often a Custom OAuth solution is required.

Custom OAuth

The basic interaction flow for a user that has not yet linked accounts is as follows:

  1. User tries to access a feature that requires authentication
  2. Alexa notifies them they need to link their account and sends a push notification to the Alexa mobile app
  3. On clicking the link in the Alexa app the user is redirect to a Cookpad login page on their phone.
  4. After successfully signing in to Cookpad the accounts are now linked, and accessing the feature from #1 above on Alexa should now work using the Cookpad user’s account.

Rails OAuth Provider with Doorkeeper

Since Cookpad’s platform is built with the Rails framework, we’re fortunate to be able to leverage the extremely robust Doorkeeper OAuth gem. To add Doorkeeper to your existing app that has user accounts and sign in, and you want to add Alexa Account Linking, read on!

1. Add the Doorkeeper Gem and create the database tables

Firstly we add the Doorkeeper gem to our app, run the install script and then generate migrations and migrate the database:

bundle add doorkeeper

rails generate doorkeeper:install

rails generate doorkeeper:migration

In addition to the gem, and the database migrations, the install script will have generated an initializer at config/initializers/doorkeeper.rb, an english language locale file config/locales/doorkeeper.en.yml as well as adding a use_doorkeeper macro to out routes file.

2. Define the Foreign Keys from the Doorkeeper tables and our User

Just before we run the migrations we need to uncomment the lines in the generated migration file to add the Foreign Keys to our existing User table. Find the new migration file and uncomment these lines at the end. For me this file is db/migrate/20221001202613_create_doorkeeper_tables.rb:

    # Uncomment below to ensure a valid reference to the resource owner's table
    add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id
    add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id

Then we can migrate the database with:

rails db:migrate

3. Connect the Doorkeeper Token tables with our User model

The existing User in our app that we will be wiring up to Amazon via Account Linking will need the associations to the new doorkeeper_access_grants and doorkeeper_access_tokens. Lets add them to our user app/models/user.rb as follows:

class User < ApplicationRecord

   (... existing code omitted ...)

   has_many :doorkeeper_access_grants,
      class_name: "Doorkeeper::AccessGrant",
      foreign_key: :resource_owner_id,
      inverse_of: false,
      dependent: :delete_all

   has_many :doorkeeper_access_tokens,
      class_name: "Doorkeeper::AccessToken",
      foreign_key: :resource_owner_id,
      inverse_of: false,
      dependent: :delete_all

   (... existing code omitted ...)
end

4. Add the Authentication Logic to the Doorkeeper Initializer

Now, in the initializer generated by the install at config/initializers/doorkeeper.rb we need to add code that will check if our user is authenticated or not in the resource_owner_authenticator block. Doorkeeper will defer to this check any time an authenticated resource is requested.

Something like below gives you the idea, just replace this with something that will retrieve the current User or redirect to the login page in your app. For example in our case, we leverage Current.user so can use that in place of User.find_by below.

resource_owner_authenticator do
   User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)
end

Doorkeeper provides a web interface for creating and managing OAuth “Applications”. In our case we’ll be creating one specifically for Alexa to authenticate. You’ll need to add the admin_authenticator block in config/initializers/doorkeeper.rb and restrict access to administrators. It should return true for an admin or otherwise false in which case it will return a 403 Forbidden response.

Use whatever implementation you like to ensure the current logged in user is an admin, eg in my case I can use Current.user.admin?, or at the most basic level something like this:

  admin_authenticator do
    User.find_by(id: session[:user_id]).admin?
  end

Lastly while we’re in there editing the initializer, lets also define the access token scopes we want to support in our service.

  default_scopes  :read, :write

Testing the Basic Doorkeeper Functionality

Now we’ve introduced Doorkeeper, we can test the basic functionality by creating an new OAuth Application via the web interface by visiting http://localhost:3000/oauth/applications/new and adding the config as shown below. Note, you’ll need to be logged in as an admin - or however you defined an admin above in the admin_authenticator block of the doorkeeper initializer.

New OAuth Application Form

Notice the redirect_uri is set to urn:ietf:wg:oauth:2.0:oob which is a special “pass through” value we can use for testing. When you submit this form, your OAuth Application should be created, including client ID and token being generated and you will see this in your browser:

Created OAuth Application Form

Now, when you click the “Authorize” button, you’ll get the Authorize/Deny screen. This is exactly the screen users will see after they click through from Amazon and try to “Link Accounts”

Authorize/Deny OAuth Authorization

Then after you click “Authorize”, you’ll see a test page that simply renders out the Authorization Code that Doorkeeper has generated for this user:

Success OAuth Authorization

This Authorization Code is what the client then needs to use to get a “real” Access Token. It may seem convoluted but this back and forth is what makes the OAuth flow secure and reliable.

So, now we have our Authorization Code - in my case here zKcGfdtsU-s3m5S2iBeE0-HwFx-sLcRkw5stvcYWNDO as shown in the above screenshot - we can just use curl to call the Token API and retrieve an Access Token! **Make sure to add your own values for client_id, client_secret and code as seen in the web UI when you created your OAuth Application.

curl -F grant_type=authorization_code \
-F client_id=j5gSZ03VkevHMI11nqXey_3zYDuZCNdWRUeryOW36Ls \
-F client_secret=DonD-RMEgWyyLy-85y-cZ188neyNXEIROW5LYL_-DZO \
-F code=zKcGfdtsU-s3m5S2iBeEO-HwFx-sLcRkw5stvcYWNDQ  \
-F redirect_uri=urn:ietf:wg:oauth:2.0:oob \
-X POST http://localhost:3000/oauth/token

If all went to plan, you should get a JSON response back similar to this that contains the actual Access Token:

{
  "access_token": "AjWRB3oHTvIHYFym7IR0sABTbu55u-bAFNE8FKcvr0w",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "read write",
  "created_at": 1685096297
}

This token can now be used by Doorkeeper to securely associate any API calls to your Rails app with the right authenticated user.

Configuring Doorkeeper and Amazon Alexa for Account Linking

Ok! Now you’ve introduced Doorkeeper to your existing Rails app, and tested it works with a dummy app, we need to move on to setting up the OAuth Provider that Amazon will use to link Alexa accounts with your user accounts in your app.

1. Creating the OAuth Provider aka Doorkeeper Application

As we did above, lets create a new OAuth Application using the web UI Doorkeeper provides at http://localhost:3000/oauth/applications/new and add the config in the same way as our test. In this case though we need to make sure to use the redirect_uris from the Alexa Developer Console account linking page. There should be three of them, and they should look something like this https://alexa.amazon.co.jp/api/skill/link/**********. Again, note you’ll need to be logged in as an admin - or however you defined an admin above in the admin_authenticator block of the doorkeeper initializer to be able to access the Doorkeeper Application management screens.

2. Connecting your Alexa Skill

The last big piece of the account linking puzzle is configuring your Alexa Skill so it knows how to connect with the OAuth provider that can link your users with Amazon’s users.

Access the Alexa Developer Console as usual here https://developer.amazon.com/alexa/console/ask and after selecting the skill you want to link, click the ‘Account Linking” menu option under “Tools”.

Here you firstly need to toggle the switch next to “Do you allow users to create an account or link to an existing account with you?” to enable the feature, then we’re going to need to add some information under “Security Provider Information” using the “Auth Code Grant” type.

Here’s an example screenshot of what you will be entering into the form, but I will explain each below.

Alexa Security Provider Information

Your Web Authorization URI: This points to your Rails app where you’ve added Doorkeeper. The default Doorkeeper authorization path is, you guessed it, /oauth/authorize, so this is where you want Amazon to send people to authenticate.

Access Token URI: Similarly, this points to your Rails app where you added Doorkeeper, and is where Amazon makes access token requests. The path is /oauth/token

Your Client ID: When you generated your Doorkeeper Application in the previous step above, Doorkeeper automatically provisioned a client ID and Secret for you. If you declared a block for admin_authenticator in the initializer as described previously, you can get get the Client ID from the applications web interface at http://localhost:3000/oauth/applications/1. Alternatively you can get this from your DB or directly from the model in the console with with Doorkeeper::Application.last.uid which should return a 43 character hash.

Your Secret: This is generated by Doorkeeper in the same way as “Your Client ID” above, so you can grab this from the applications web interface, or the Rails console with Doorkeeper::Application.last.secret and drop the resulting hash into the Alexa configuration form.

Your Authentication Scheme: This is the auth scheme that Amazon will expect to find when they request tokens from their platform. Just leave this as the default “HTTP Basic”.

Scope: Scopes allow us to define more granularly what kind of permissions we want to provide to clients. Since we want to allow Alexa to “read” and “write” data from and to our platform, we configured these scopes in #4 above. So to match that we’ll add “read” and “write” in this section of the form in the Alexa Developer Console.

Domain List: This is an allowlist of domains that the authorization URL will fetch content from. In our case, we just have a single Doorkeeper OAuth provider service in our Rails app, so we’ll add our domain here your-domain-here.com.

Default Access Token Expiration Time: If the OAuth provider does not set a token expiry time, then we can set a default here. Doorkeeper sets this to 2 hours by default, so we can leave this blank here.

Alexa Redirect URLs: Lastly, you should see 3 URLs listed. These are the endpoints that we need to redirect flow back to from our app after they have authenticated. You should have added these URLs in the previous step above when creating the Doorkeeper Application with Doorkeeper::Application.create.

Test Account Linking from your Alexa Skill

Now we can actually try to link accounts! By leveraging ngrok we can actually test linking an Alexa Skill with our Rails app running on localhost. First, install ngrok if needed, and then start it up to serve HTTP over port 3000:

ngrok http 3000

When you do this, ngrok will spin up a session as shown below. You can use it for free but be aware the session will expire after 2 hours.

Session Status                online
Session Expires               1 hour, 59 minutes
Terms of Service              https://ngrok.com/tos
Version                       3.3.0
Region                        Europe (eu)
Latency                       39ms
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://8ad4-77-101-215-47.eu.ngrok.io -> http://localhost:3000

So now we have an external HTTPS url that will forwarded traffic to our Rails app running on localhost! In my case above its https://8ad4-77-101-215-47.eu.ngrok.io.

We can now temporarily update our Skill configuration in the Alexa Developer Console to point to this URL, so then we can link the Skill Amazon account with an account on our local machine!

Open the Alexa App on your phone and navigate to your skill setting page. You should now see a “Link Account” link in the top right. Clicking this will send you over to your localhost webapp where you can sign in and link accounts!

Once you’ve done this and deployed your app to production users with Doorkeeper configured, don’t forget to update the Authorization and Token URIs in the account linking section of the Alexa Developer Console to point to your real domain rather than ngrok on your local machine!

Accessing Authenticated Resources in your Webapp

Now we’ve successfully linked accounts, every time Amazon makes a request to our Skill Lambda, it will include the user’s access_token in the JSON request as shown below. You can get this token via the context object via context.System.user.accessToken

"user": {
   "userId": "amzn1.ask.account.XXXXXXXXXX",
   "accessToken": "Atza|<accessToken>"
},

Now you can make requests from the Alexa Lambda back to your Rails app, passing the access token, and the Rails app can associate the token with a User in that system. For example you might want to show a list of articles that a user bookmarked in your webapp on their Alexa screen, or be able to pull up a shopping cart on Alexa to review items added in your webapp.

To enforce authentication on any resource in your Rails app you simply need a before action in your controller to protect that resource and check the access_token was present in the request

before_action :doorkeeper_authorize!
  1. Doorkeeper Rails Guide: https://doorkeeper.gitbook.io/guides/ruby-on-rails/getting-started
  2. Alexa Developer Console: https://developer.amazon.com/alexa/console/ask
  3. Alexa Account Linking for Custom Skills: https://developer.amazon.com/en-US/docs/alexa/account-linking/account-linking-for-custom-skills.html