Duo Dealer App

Version Build Status

Duo Dealer Application Rails engine and generator

NOTE : Versions 8.0.0 through 8.2.3 contained a CSRF vulnerability that was addressed in version 8.2.4. Please update to version 8.2.4 if you're using an old version.

Table of Contents

Introduction

Get started with the Duo Dealer Admin API faster; This gem includes a Rails Engine and generators for writing Rails applications using the Duo Dealer API. The Engine provides a SessionsController and all the required code for authenticating with a shop via Oauth (other authentication methods are not supported).

Note: It's recommended to use this on a new Rails project, so that the generator won't overwrite/delete your files.

Learn how to create and deploy a new Duo Dealer App to Heroku with our quickstart guide, or dive in in less than 5 minutes with this quickstart video:

https://www.youtube.com/watch?v=yGxeoAHlQOg

Become a Duo Dealer App Developer

To become a Duo Dealer App Developer you'll need a Duo Dealer Partner account. If you don't have a Duo Dealer Partner account, head to http://duodealer.com/partners to create one before you start.

Once you have a Partner account, create a new application in the Partner Dashboard to get an API key and other API credentials.

To create an application for development set your new app's App URL to the URL provided by your tunnel, ensuring that you use https://. If you are not planning to embed your app inside the Duo Dealer admin or receive webhooks, set your redirect URL to http://localhost:3000/ and the Whitelisted redirection URL(s) to contain <App URL>/auth/duodealer/callback.

Installation

To get started add duodealer_app to your Gemfile and run bundle install:

# Create a new rails app
$ rails new my_duodealer_app
$ cd my_duodealer_app

# Add the gem duodealer_app to your Gemfile
$ echo "gem 'duodealer_app'" >> Gemfile
$ bundle install

Now we are ready to run any of the generators included with duodealer_app. The following section explains the generators and what you can do with them.

Rails Compatibility

The lastest version of duodealer_app is compatible with Rails >= 5. Use version <= v7.2.8 if you need to work with Rails 4.

Generators

Default Generator

The default generator will run the install, shop, and home_controller generators. This is the recommended way to start a new app from scratch:

$ rails generate duodealer_app

After running the generator, you will need to run rails db:migrate to add new tables to your database. You can start your app with bundle exec rails server and install your app by visiting http://localhost in your web browser.

API Keys

The default and install generators have been updated to source Duo Dealer API key and secret from an Environment (.env) variables file, which you will need to create with the following format:

DUODEALER_API_KEY=your api key
DUODEALER_API_SECRET=your api secret

These values can be found on the "App Setup" page in the Duo Dealer Partners Dashboard. If you are checking your code into a code repository, ensure your .gitignore prevents your .env file from being checked into any publicly accessible code.

Install Generator

$ rails generate duodealer_app:install

# or optionally with arguments:

$ rails generate duodealer_app:install

Other options include:

  • application_name - the name of your app, it can be supplied with or without double-quotes if a whitespace is present. (e.g. --application_name Example App or --application_name "Example App")
  • scope - the Oauth access scope required for your app, eg read_products, write_orders. Multiple options need to be delimited by a comma-space, and can be supplied with or without double-quotes (e.g. --scope read_products, write_orders, write_products or --scope "read_products, write_orders, write_products") For more information, refer the docs.
  • embedded - the default is to generate an embedded app, if you want a legacy non-embedded app then set this to false, --embedded false

You can update any of these settings later on easily, the arguments are simply for convenience.

The generator adds DuodealerApp and the required initializers to the host Rails application.

After running the install generator, you can start your app with bundle exec rails server and install your app by visiting localhost.

Home Controller Generator

$ rails generate duodealer_app:home_controller

This generator creates an example home controller and view which fetches and displays products using the Duo Dealer API

App Proxy Controller Generator

$ rails generate duodealer_app:app_proxy_controller

This optional generator, not included with the default generator, creates the app proxy controller to handle proxy requests to the app from your shop storefront, modifies 'config/routes.rb' with a namespace route, and an example view which displays current shop information using the LiquidAPI

Marketing Extension Generator

$ rails generate duodealer_app:add_marketing_activity_extension

This will create a controller with the endpoints required to build a marketing activities extension. The extension will be generated with a base url at /marketing_activities, which should also be configured in partners.

Controllers, Routes and Views

The last group of generators are for your convenience if you want to start overriding code included as part of the Rails engine. For example by default the engine provides a simple SessionController, if you run the rails generate duodealer_app:controllers generator then this code gets copied out into your app so you can start adding to it. Routes and views follow the exact same pattern.

Mounting the Engine

Mounting the Engine will provide the basic routes to authenticating a shop with your application. By default it will provide:

Verb Route Action
GET '/login' Login
POST '/login' Login
GET '/auth/duodealer/callback' Authenticate Callback
GET '/logout' Logout
POST '/webhooks/:type' Webhook Callback

Nested Routes

The engine may also be mounted at a nested route, for example:

mount DuodealerApp::Engine, at: '/nested'

This will create the Duo Dealer engine routes under the specified subpath. You'll also need to make some updates to your duodealer_app.rb and omniauth.rb initializers. First update the duodealer_app initializer to include a custom root_url e.g.:

DuodealerApp.configure do |config|
  config.root_url = '/nested'
end

then update the omniauth initializer to include a custom callback_path e.g.:

provider :duodealer,
  DuodealerApp.configuration.api_key,
  DuodealerApp.configuration.secret,
  scope: DuodealerApp.configuration.scope,
  callback_path: '/nested/auth/duodealer/callback'

You may also need to change your config/routes.rb to render a view for /nested, since this is what will be rendered in the Duo Dealer Admin of any shops that have installed your app. The engine itself doesn't have a view for this, so you'll need something like this:

# config/routes.rb
Rails.application.routes.draw do
  root :to => 'something_else#index'
  get "/nested", to: "home#index"
  mount DuodealerApp::Engine, at: '/nested'
end

Finally, note that if you do this, to add your app to a store, you must navigate to /nested in order to render the Enter your shop domain to log in or install this app. UI.

Custom login URL

While you can customize the login view by creating a /app/views/duodealer_app/sessions/new.html.erb file, you may also want to customize the URL entirely. You can modify your duodealer_app.rb initializer to provide a custom login_url e.g.:

DuodealerApp.configure do |config|
  config. = 'https://my.domain.com/nested/login'
end

Authentication

DuodealerApp::SessionRepository

DuodealerApp::SessionRepository allows you as a developer to define how your sessions are stored and retrieved for shops. The SessionRepository is configured in the config/initializers/duodealer_app.rb file and can be set to any object that implements self.store(auth_session, *args) which stores the session and returns a unique identifier and self.retrieve(id) which returns a DuodealerAPI::Session for the passed id. These methods are already implemented as part of the DuodealerApp::SessionStorage concern, but can be overridden for custom implementation.

If you only run the install generator then by default you will have an in memory store but it won't work on multi-server environments including Heroku. For multi-server environments, implement one of the following token-storage strategies.

Shop-based token storage

Storing tokens on the store model means that any user login associated to the store will have equal access levels to whatever the original user granted the app.

$ rails generate duodealer_app:account_model

This will generate a shop model which will be the storage for the tokens necessary for authentication.

User-based token storage

A more granular control over level of access per user on an app might be necessary, to which the shop-based token strategy is not sufficient. Duo Dealer supports a user-based token storage strategy where a unique token to each user can be managed.

$ rails generate duodealer_app:user_model

This will generate a user model which will be the storage for the tokens necessary for authentication.

The current Duo Dealer user will be stored in the rails session at session[:duodealer_user]

In this mode, The self.store(auth_session, *args) will be invoked with a Duo Dealer User object hash, which is then used to store the token as part of a user record, rather than a store record.

This will change the type of token that Duo Dealer returns and it will only be valid for a short time. Read more about Online access here. Note that this means you won't be able to use this token to respond to Webhooks.

Migrating from shop-based to user-based token strategy

After running the generator, ensure that configuration settings are successfully changed:

# In the `omniauth.rb` initializer:
provider :duodealer,
  DuodealerApp.configuration.api_key,
  DuodealerApp.configuration.secret,
  scope: DuodealerApp.configuration.scope,
  per_user_permissions: true

# In the `duodealer_app.rb` initializer:
config.session_repository = 'User'
config.per_user_tokens = true

Authenticated

The engine provides a DuodealerApp::Authenticated concern which should be included in any controller that is intended to be behind Duo Dealer OAuth. It adds before_actions to ensure that the user is authenticated and will redirect to the Duo Dealer login page if not. It is best practice to include this concern in a base controller inheriting from your ApplicationController, from which all controllers that require Duo Dealer authentication inherit.

For backwards compatibility, the engine still provides a controller called DuodealerApp::AuthenticatedController which includes the DuodealerApp::Authenticated concern. Note that it inherits directly from ActionController::Base, so you will not be able to share functionality between it and your application's ApplicationController.

AfterAuthenticate Job

If your app needs to perform specific actions after the user is authenticated successfully (i.e. every time a new session is created), DuodealerApp can queue or run a job of your choosing (note that we already provide support for automatically creating Webhooks and Scripttags). To configure the after authenticate job update your initializer as follows:

DuodealerApp.configure do |config|
  config.after_authenticate_job = { job: "Duo Dealer::AfterAuthenticateJob" }
end

The job can be configured as either a class or a class name string.

If you need the job to run synchronously add the inline flag:

DuodealerApp.configure do |config|
  config.after_authenticate_job = { job: Duo Dealer::AfterAuthenticateJob, inline: true }
end

We've also provided a generator which creates a skeleton job and updates the initializer for you:

bin/rails g duodealer_app:add_after_authenticate_job

If you want to perform that action only once, e.g. send a welcome email to the user when they install the app, you should make sure that this action is idempotent, meaning that it won't have an impact if run multiple times.

API Versioning

Duo Dealer's API is versioned, and you can read about that process in the Duo Dealer Developers documentation page.

Since duodealer_app gem version 1.11.0, the included duodealer_api gem has also been updated to allow you to easily set and switch what version of the Duo Dealer API you want your app or service to use, as well as surface warnings to Rails apps about deprecated endpoints, GraphQL fields and more.

See the duodealer_api gem README for more details.

WebhooksManager

DuodealerApp can manage your app's webhooks for you if you set which webhooks you require in the initializer:

DuodealerApp.configure do |config|
  config.webhooks = [
    {topic: 'carts/update', address: 'https://example-app.com/webhooks/carts_update'}
  ]
end

When the oauth callback is completed successfully DuodealerApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every oauth callback it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.

DuodealerApp also provides a WebhooksController that receives webhooks and queues a job based on the received topic. For example if you register the webhook from above then all you need to do is create a job called CartsUpdateJob. The job will be queued with 2 params: account_domain and webhook (which is the webhook body).

If you would like to namespace your jobs you may set webhook_jobs_namespace in the config. For example if your app handles webhooks from other ecommerce applications as well, and you want Duo Dealer cart update webhooks to be processed by a job living in jobs/duodealer/webhooks/carts_update_job.rb rather than jobs/carts_update_job.rb):

DuodealerApp.configure do |config|
  config.webhook_jobs_namespace = 'duodealer/webhooks'
end

If you are only interested in particular fields, you can optionally filter the data sent by Duo Dealer by specifying the fields parameter in config/webhooks. Note that you will still receive a webhook request from Duo Dealer every time the resource is updated, but only the specified fields will be sent.

DuodealerApp.configure do |config|
  config.webhooks = [
    {topic: 'products/update', address: 'https://example-app.com/webhooks/products_update', fields: ['title', 'vendor']}
  ]
end

If you'd rather implement your own controller then you'll want to use the WebhookVerification module to verify your webhooks, example:

class CustomWebhooksController < ApplicationController
  include DuodealerApp::WebhookVerification

  def carts_update
    params.permit!
    SomeJob.perform_later(account_domain: , webhook: webhook_params.to_h)
    head :no_content
  end

  private

  def webhook_params
    params.except(:controller, :action, :type)
  end
end

The module skips the verify_authenticity_token before_action and adds an action to verify that the webhook came from Duo Dealer. You can now add a post route to your application pointing to the controller and action to accept the webhook data from Duo Dealer.

The WebhooksManager uses ActiveJob, if ActiveJob is not configured then by default Rails will run the jobs inline. However it is highly recommended to configure a proper background processing queue like sidekiq or resque in production.

DuodealerApp can create webhooks for you using the add_webhook generator. This will add the new webhook to your config and create the required job class for you.

rails g duodealer_app:add_webhook -t carts/update -a https://example.com/webhooks/carts_update

where -t is the topic and -a is the address the webhook should be sent to.

ScripttagsManager

As with webhooks, DuodealerApp can manage your app's scripttags for you by setting which scripttags you require in the initializer:

DuodealerApp.configure do |config|
  config.scripttags = [
    {event:'onload', src: 'https://my-duodealerapp.herokuapp.com/fancy.js'},
    {event:'onload', src: ->(domain) { dynamic_tag_url(domain) } }
  ]
end

You also need to have write_script_tags permission in the config scope in order to add script tags automatically:

 config.scope = '... , write_script_tags'

Scripttags are created in the same way as the Webhooks, with a background job which will create the required scripttags.

If src responds to call its return value will be used as the scripttag's source. It will be called on scripttag creation and deletion.

RotateDuo DealerTokenJob

If your Duo Dealer secret key is leaked, you can use the RotateDuo DealerTokenJob to perform API Credential Rotation.

Before running the job, you'll need to generate a new secret key from your Duo Dealer Partner dashboard, and update the /config/initializers/duodealer_app.rb to hold your new and old secret keys:

config.secret = Rails.application.secrets.duodealer_secret
config.old_secret = Rails.application.secrets.old_duodealer_secret

We've provided a generator which creates the job and an example rake task:

bin/rails g duodealer_app:rotate_duodealer_token_job

The generated rake task will be found at lib/tasks/duodealer/rotate_duodealer_token.rake and is provided strictly for example purposes. It might not work with your application out of the box without some configuration.

⚠️ Note: if you are updating duodealer_app from a version prior to 8.4.2 (and do not wish to run the default/install generator again), you will need to add the following line to config/intializers/omniauth.rb:

strategy.options[:old_client_secret] = DuodealerApp.configuration.old_secret

App Tunneling

Your local app needs to be accessible from the public Internet in order to install it on a Duo Dealer store, to use the App Proxy Controller or receive Webhooks.

Use a tunneling service like ngrok, Forward, Beeceptor, Mockbin, or Hookbin to make your development environment accessible to the internet.

For example with ngrok, run this command to set up a tunnel proxy to Rails' default port:

ngrok http 3000

AppProxyVerification

The engine provides a mixin for verifying incoming HTTP requests sent via an App Proxy. Any controller that includes DuodealerApp::AppProxyVerification will verify that each request has a valid signature query parameter that is calculated using the other query parameters and the app's shared secret.

The App Proxy Controller Generator automatically adds the mixin to the generated app_proxy_controller.rb Additional controllers for resources within the App_Proxy namespace, will need to include the mixin like so:

# app/controllers/app_proxy/reviews_controller.rb
class ReviewsController < ApplicationController
  include DuodealerApp::AppProxyVerification
  # ...
end

Create your app proxy url in the Duo Dealer Partners' Dashboard, making sure to point it to https://your_app_website.com/app_proxy. Creating an App Proxy

App Bridge

A basic example of using App Bridge is included in the install generator. An app instance is automatically initialized in duodealer_app.js and flash_messages.js converts Rails flash messages to App Bridge Toast actions automatically. By default, this library is included via unpkg in the embedded_app layout. For more advanced uses it is recommended to install App Bridge via npm or yarn.

Troubleshooting

see TROUBLESHOOTING.md

Testing an embedded app outside the Duo Dealer admin

By default, loading your embedded app will redirect to the Duo Dealer admin, with the app view loaded in an iframe. If you need to load your app outside of the Duo Dealer admin (e.g., for performance testing), you can change forceRedirect: true to false in DuodealerApp.init block in the embedded_app view. To keep the redirect on in production but off in your development and test environments, you can use:

forceRedirect: <%= Rails.env.development? || Rails.env.test? ? 'false' : 'true' %>

Questions or problems?

Upgrading to 11.7.0

Session storage method signature breaking change

If you override def self.store(auth_session) method in your session storage model (e.g. Shop), the method signature has changed to def self.store(auth_session, *args) in order to support user-based token storage. Please update your method signature to include the second argument.

Rails 6 Compatibility

Disable Webpacker

If you are using sprockets in rails 6 or want to generate a duodealer_app without webpacker run the install task by running

DUODEALER_APP_DISABLE_WEBPACKER=1 rails generate duodealer_app

and then in your DuodealerApp configuration block, add

DuodealerApp.configure do |config|
  config.disable_webpacker = true
end

Upgrading from 8.6 to 9.0.0

Configuration change

Add an api version configuration in config/initializers/duodealer_app.rb Set this to the version you want to run against by default. See Duo Dealer API docs for versions available.

config.api_version = '2019-04'

Session storage change

You will need to add an api_version method to you session storage object. The default implementation for this is.

def api_version
  DuodealerApp.configuration.api_version
end

Generated file change

embedded_app.html.erb the usage of account_session.url needs to be changed to account_session.domain

<script type="text/javascript">
  DuodealerApp.init({
    apiKey: "<%= DuodealerApp.configuration.api_key %>",

    shopOrigin: "<%= "https://#{ @account_session.url }" if @account_session %>",

    debug: false,
    forceRedirect: true
  });
</script>

is changed to

<script type="text/javascript">
  DuodealerApp.init({
    apiKey: "<%= DuodealerApp.configuration.api_key %>",

    shopOrigin: "<%= "https://#{ @account_session.domain }" if @account_session %>",

    debug: false,
    forceRedirect: true
  });
</script>

DuodealerAPI changes

You will need to also follow the DuodealerAPI upgrade guide to ensure your app is ready to work with api versioning.