Building an OAuth App for BigCommerce using Rails | Part 2: Creating a Rails 7 App and Connecting to BigCommerce

John Hebron
20 min readDec 16, 2023

--

In Part 1, we set up all of the necessary third party services and tools that we will need to scaffold our app.

In this, Part 2 of our series on Building a Rails OAuth app for BigCommerce, we will create a brand new Rails 7 app, set up the necessary configuration to connect to our BigCommerce account(s) and ngrok, and add the code to work with the BigCommerce Omniauth and API gems.

When we’re done, we’ll have a BigCommerce app, running locally on our computer but accessible to the internet via ngrok, which we can install and open on our BigCommerce store.

I hope you’re excited! Let’s go!

Overview

  1. Install a new Rails 7.0.7.2 app locally
  2. Set a home page for our Rails app
  3. Update our Rails app to work with ngrok
  4. Edit our gemfile / Add new gems
  5. Set up our local environment variables
  6. Update the app settings in the BigCommerce Developer Center
  7. Update our Application Controller for BigCommerce API requests
  8. Create a user and store model and table
  9. Set up the `omniauth-bigcommerce` gem
  10. Create our authentication routes and controller
  11. Test out our app

1. Install a new Rails 7.0.7.2 app locally

I’ll be using Rails 7.0.7.2 and Ruby 3.1.3 in my app. You should probably use the same if you want to follow along without any hiccups, but earlier (or later) versions of Rails may have very similar steps.

Screenshot of Ruby and Rails versions

If you need to change your version of Ruby, I recommend using a Ruby Version Manager, like RVM, to install and update Ruby versions on your system. Once RVM is installed, you can install Ruby 3.1.3 with the following command:

rvm install ruby-3.1.3

If you don’t already have Rails installed, check out their guide to get it up and running. I’ll be working on a Mac but many, if not all, of the steps should be the same for a PC.

In your terminal, cd into the directory where you want to create your project. Mine lives in ~/Dev but yours can live anywhere.

To create a new Rails 7.1.2 application called “my-big-app”, type into the terminal:

rails _7.1.2_ new my-big-app
Screenshot by Author

Notice how we are specifying the version of Rails with _7.1.2_, followed by the new command and then the name of our app, my-big-app in this case.

If you’ve never run this on your computer before, this may take a few minutes while it downloads and installs all of the gems required to run Rails. Feel free to grab a cup of coffee or walk the dog while this runs for the first time.

Once that’s done, you should have a barebones Rails app installed and ready!

In the terminal, cd into your new directory (mine is called my-big-app) and spin up a Rails server by typing bin/rails s from within the directory.

The server should boot and you can copy the 127.0.0.1:3000 part of the URL (yours may differ) to access it in the browser, where you should see a Rails landing page. If so, well done, you’re up and running!

To exit the server, just hit control + c in your terminal on a Mac.

2. Set a home page for your Rails app

The default landing page is great and all, but let’s create a custom home page for our app really quick.

First, let’s generate a controller which we will call home .

bin/rails generate controller home

Next, we’ll add a new view for our Index page by creating a new file at app/views/home/index.html.erb.

Note that the filename has two extensions: .html.erb. This indicates to Rails that it is an Embedded Ruby template and contains HTML and Ruby code.

Within that file, let’s add a little HTML like so:

<h1>Home Page</h1>
<p>Welcome to my home page!</p>
<p>One day I'll be a BigCommerce app!</p>

Wonderful! Now we just need to set up a route to let Rails know how to reach our #index, and we are good to go!

Let’s open config/routes.rb and add the following line:

root 'home#index'

Now, Rails will know that you want the root of your website to be directed to the #index action on the homecontroller. And thanks to “Rails magic”, it already knows that the app/views/home/index.html.erb template matches this action.

So, voila! Let’s start our server again and view our homepage. You should now see your template displayed.

bin/rails s

3. Update our Rails app to work with ngrok

Our beautiful little app is up and running, but only on our local machine. If you were to give a stranger on the internet the address “127.0.0.1:3000” they wouldn’t be able to see your beautiful new site.

That’s because the built in Rails server ( bin/rails s) serves your site on your local network via port 3000, not publicly on the internet.

This is where ngrok comes in the help. In Part 1 of this series, you set up a new ngrok account and installed ngrok onto your local machine. Let’s try using that ngrok account/CLI to make our Rails server accessible online.

Starting your Rails app and ngrok

We’re going to open two terminal windows; one running our Rails app and one runnin the ngrok command line tool.

In one terminal window, start your Rails server in your app directory so that the Rails app is running locally on your machine (defaulting to port 3000).

bin/rails server

In another terminal window, let’s start the ngrok service, letting it know that we want to use our static domain (see Part 1) and that we want to expose the traffic on port 3000 of our local machine.

Your command will look like this, but your static domain will be different:

ngrok http --domain=allowed-uniquely-mouse.ngrok-free.app 3000

Now, we should be able to pull up our ngrok provided url, in my case https://allowed-uniquely-mouse.ngrok-free.app/, and ngrok will now allow us to access the Rails app running on port 3000 of our local computer, but from any internet connected computer!

To prove this works, I can copy that url, visit it in my browser, and let’s see what we get.

Upon first visit, you’ll likely be presented with a warning page explaining that this page you are visiting is being redirected through ngrok and that, if you’re not the developer who set this up, then you’re likely in the wrong place!

You can go ahead and click “Visit Site” to accept/acknowledge this message and move forward.

Ok, one step closer! Now we get an error that the host is being blocked. Rails 6 added the concept of “blocked hosts” for DNS rebinding, so that’s what we are running into here. Essentially, Rails is trying to be smart and say “there’s some weird DNS going on and I’m not having it.”

But, since *we* set up the DNS, we know that it’s ok to use and thus we can override this block for the ngrok domain.

Here’s a short and sweet article on blocked hosts for Rails 6, but essentially we just need to update our development.rb file to include the ngrok domain pattern.

Let’s open up config/environments/development.rb and inside of the Rails configuration block, let’s add in a config.hosts entry to allow the ngrok-free.app domain.

Rails.application.configure do
# lots of stuff...
config.hosts << /([A-Za-z0-9_-]+)\.ngrok-free\.app/
end

Now, let’s stop our rails server with control + c and then start it up again with bin/rails s and let’s visit that ngrok domain one more time.

Perfect!

We are now ready to go with a way of securely accessing our local Rails app from anywhere online! This is going to be crucial in a moment when we update our app for the BigCommerce authentication flow.

4. Edit our gemfile / Add new gems

In Part 1, we set up a BigCommerce store and a BigCommerce draft application. From above, we now have a Rails application available online via ngrok. Now it’s time to start entering our environment details to connect each to one another.

For our local Rails app, we’ll want to install a few gems and set up a .env file to hold our configuration information. The .env file will serve as a place for us to store “environment variables” and “secrets” for our app to use.

Open your Rails project in your favorite editor (mine is RubyMine) and open your Gemfile. We are going to make a few tweaks here.

First, at the bottom of your Gemfile, look for a group that contains :development, :test. Inside that do/end block, we are going to add the dotenv-railsgem. This will allow us to use a local .env file in only our development and test environments.

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ]

# Adding our gems below:
gem 'dotenv-rails', require: 'dotenv/rails-now'
end

Finally, we need the bigcommerce gem and the omniauth-bigcommerce gem to help us connect to BigCommerce. We need these in all environments, so let’s add them to the top of our Gemfile right below where the rails gem is listed.

gem 'bigcommerce', '~> 1.0'
gem 'omniauth-bigcommerce'

In the end, your Gemfile should look something like this (I’ve simplified mine to show just the changes we have made):

source "https://rubygems.org"

ruby "3.1.3"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.2"

# Adding BigCommerce specific gems
gem 'bigcommerce', '~> 1.0'
gem 'omniauth-bigcommerce'

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ]

# Adding our gems below:
gem 'dotenv-rails', require: 'dotenv/rails-now'
end

Perfect! To recap, these changes will do the following:

  1. In the development and test environments, it will install dotenv-rails ,which will allow us to use and read from a .env file
  2. It will install the gems bigcommerce and omniauth-bigcommerce for all environments.

Now we can install all of the gems. If your Rails server is still running, be sure to stop it first with a ctrl + c.

Jump back in the terminal, make sure you are in the project directory, and type bundle install. This should run through the Gemfile and install all of the new gems and their dependencies.

Beautiful. Now that we have that set up, we can test and make sure that our local environment is still working. Let’s hop back in the terminal and try starting our server again with bin/rails s . You should see your server load perfectly!

Alright, the final step for our local environment is to set up that .env file I mentioned which will hold your environment variables and secrets for local development.

In your root directory of your app, create a new file and name it .env .

You can do that from the terminal by typing touch .env . Notice there is no filename, just the .env extension.

In your text editor, let’s open up this .env file, which should be empty.

We are going to place a few environment variables in this file in just a second.

High five!

5. Set up our local environment variables

In order to connect to BigCommerce, there are a few environment variables that will need to be present locally (and later in production).

These variables have been specified by the bigcommerce and omniauth-bigcommerce gems and will be required for using these gems to authenticate and communicate with the BigCommerce API.

Let’s set those up now.

Open your .env file and add the following variables:

BC_CLIENT_ID=
BC_CLIENT_SECRET=
BC_REDIRECT_URI=
APP_URL=

Setting your BC Client ID and Client Secret

Your BC_CLIENT_ID and BC_CLIENT_SECRET can be found at https://devtools.bigcommerce.com/my/apps . You should see your app displayed on the home page under Drafts (which we created earlier); click View Client ID.

In a pop up modal, you should see your Client ID and Client Secret.

Copy and paste these values into your .env file. No quotation marks or spaces are needed around the values. Mine looks like this so far:

Setting your APP_URL

For your APP_URL, we’re going to use the ngrok static domain we’ve been using, with no trailing / “slash” character.

If you remember, for me, that domain was https://allowed-uniquely-mouse.ngrok-free.app

Now, my .env file looks like the following:

Setting your BC Redirect URI

Finally, we need to set the BC_REDIRECT_URI. This variable will consist of your ngrok domain name plus /auth/bigcommerce/callback .

This is the route that the omniauth-bigcommerce gem will be using by default, so that’s where these seemingly arbitrary domain paths come from.

In your .env file, update your BC_REDIRECT_URI as such, making sure to not use a trailing / “slash”.

Here’s what my final .env file looks like.

6. Update your app settings in the BigCommerce Developer Center

The last step is to update our BigCommerce app settings at https://devtools.bigcommerce.com/my/apps to let it know our ngrok domain.

Let’s find our app again, under Drafts, and click Edit App.

On the “1 Technical” tab, I’m going to update the Auth , Load, and Uninstall Callback URLs with my APP_URL as the base. They will look like such:

Auth Callback URL: 
https://allowed-uniquely-mouse.ngrok-free.app/auth/bigcommerce/callback
Auth Load URL: 
https://allowed-uniquely-mouse.ngrok-free.app/load
Auth Uninstall URL: 
https://allowed-uniquely-mouse.ngrok-free.app/uninstall
Screenshot by Author

Make sure you use the same paths that I’m using. If your Auth Callback URL is different, the Rails magic won’t happen for you later on. Watch for those trailing / “slashes”.

Note that this is also the page where you will set the different permissions or “scopes” for your app. Once you’re ready to build your app out, you’ll come here to decide which scopes you need. Easy peasy.

Since we might want to add a little blurb on our homepage with the store’s name, let’s look for the “Information and Settings” scope and select Read-Only.

Now scroll to the bottom and click “Update and Close.”

At this point, you have a working Rails 7 app, accessible online via ngrok, with all the required BigCommerce accounts, credentials, and environment variables required to make your installed BigCommerce gems do their magic. Congratulations!

Now, it’s time to finesse our Rails app for BigCommerce specific considerations and to set up the required routes, models, and controllers to make our omniauth-bigcommerce gem work. Onward!

7. Update our Rails Application Controller for BigCommerce API requests

Since BigCommerce apps are served in iframes within the BigCommerce dashboard, we’ll need to delete the X-Frame-Options header from our requests to prevent an issue.

Open up app/controllers/application_controller.rb and add the following code:

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
after_action :set_header_for_iframe

private

def set_header_for_iframe
response.headers.delete "X-Frame-Options"
end
end

This will make sure the X-Frame-Options header is removed before sending an outgoing request.

8. Create 'user’ and 'store’ models and tables

In order to save the data from our auth process, and to know who is using our app, we’ll need User and Store models.

Let’s start with our Store model and table.

We’ll need fields for :user_id, :scopes, :store_hash, :access_token, and is_installed (which will help us track if an app is currently installed in a store).

Let’s create a new model and migration all-in-one:

rails g model Store user_id:integer scopes:string store_hash:string access_token:string is_installed:boolean

Now let’s set up the User model and migration. All we need for the User model right now are fields for bc_id, name, and email.

rails g model User bc_id:string name:string email:string

Lastly, we’ll edit each model with references and validations.

In app/models/store.rb we will add the following has_many relationship with users and validation on the access_token and store_hash.

class Store < ApplicationRecord
has_many :users

validates_presence_of :access_token, :store_hash
validates_uniqueness_of :store_hash
end

In app/models/user.rb we will add the following validations on the bc_id, name , and email.

class User < ApplicationRecord
validates_presence_of :bc_id, :name, :email
validates_uniqueness_of :bc_id
end

Great! Now, let’s run our migrations!

bin/rails db:migrate

We’re getting there so quickly!

9. Set up the ‘omniauth-bigcommerce’ gem

Per the omniauth-bigcommerce GitHub Readme, we need to create a config/initializers/omniauth.rb initializer.

The file should look like the following:

Rails.application.config.middleware.use OmniAuth::Builder do
provider :bigcommerce, ENV['BC_CLIENT_ID'], ENV['BC_CLIENT_SECRET']
OmniAuth.config.full_host = ENV['APP_URL'] || nil
end

This tells the omniauth-bigcommerce gem to use the bigcommerce provider and pulls our BigCommerce Client ID, Client Secret, and App URL from our environment variables to configure the omniauth gem.

This will allow the gem to do all of the “handshake” work of authenticating our app with the BigCommerce API.

Note: Normally, this would be plenty to get the omniauth gem working, but there seems to be a known issue right now causing an “Invalid Client ID” error if the gem upgrades its dependency on Oauth2 to v2 or above.

To fix this, we just need to explicitly pass our token params in the Omniauth initializer. Our final /config/initializers/omniauth.rb file will look like this:

Rails.application.config.middleware.use OmniAuth::Builder do
provider :bigcommerce, ENV['BC_CLIENT_ID'], ENV['BC_CLIENT_SECRET'],
{
token_params: {
client_id: ENV['BC_CLIENT_ID'],
client_secret: ENV['BC_CLIENT_SECRET']
}
}
OmniAuth.config.full_host = ENV['APP_URL'] || nil
end

10. Create our authentication routes and controller

We need a few routes to handle the OAuth process. Update your config/routes.rb with the following:

# Omniauth Routes
get '/auth/failure', to: 'sessions#failure'
get '/auth/:name/callback', to: 'sessions#new'

# BigCommerce App Routes
get '/load', to: 'sessions#show'
get '/uninstall', to: 'sessions#destroy'
get 'remove-user', to: 'sessions#remove'

This creates the two routes needed by the Omniauth gem and the three routes expected by the BigCommerce App store for installing, loading, and uninstalling our app (remember when we entered those URLS earlier in the App settings?)

Now let’s create the controller to handle our OAuth flow. We’ll call it sessions to match the routes we’ve created.

bin/rails g controller Sessions

Perfect. Now let’s edit our app/controllers/sessions_controller.rb and add in our corresponding methods for our routes.

def new
end

def show
end

def destroy
end

def remove
end

def failure
end

Now we need to set up the code in each method to handle the actual request. This is a hefty amount of code, so I’m providing a link to my GitHub repo at the end of the article.

Hopefully the code is commented well enough to explain to you what’s happening. Here’s a high-level overview:

def new

def new
# OmniAuth magic
auth = request.env['omniauth.auth']

# If, for some reason, auth fails and isn't redirected,
# we will catch it here
unless auth && auth[:extra][:raw_info][:context]
render render_error "[install] Invalid credentials: #
{JSON.pretty_generate(auth[:extra])}"
end

# Set up our variables we will need to generate a User and a Store
email = auth[:info][:email]
name = auth[:info][:name]
bc_id = auth[:uid]
store_hash = auth[:extra][:context].split('/')[1]
token = auth[:credentials][:token]
scopes = auth[:extra][:scopes]

# Look for existing store by store_hash
store = Store.where(store_hash: store_hash).first

if store
logger.info "[install] Updating token for store '#{store_hash}'
with scope '#{scopes}'"
store.update(access_token: token,
scopes: scopes,
is_installed: true)
else
# Create store record
logger.info "[install] Installing app for store '#{store_hash}'
with admin '#{email}'"
store = Store.create(store_hash: store_hash,
access_token: token,
scopes: scopes,
is_installed: true)
end

# Find or create user by email
user = User.where(email: email).first_or_create do |user|
user.bc_id = bc_id
user.name = name
user.save!
end

# Other one-time installation provisioning goes here.

# Login and redirect to home page
session[:store_id] = store.id
session[:user_id] = user.id
redirect_to '/'
end

In the new action, we are using some OmniAuth magic to authenticate our user using the omniauth.auth GET variable BigCommerce provides.

If, for some reason, we authenticate successfully but have no context, then we fail. The context is essentially the ID of the store, also known as the store_hash .

The render_error method is just a helper method we’ve added at the bottom of the controller to render a json response with our error message and a request to reload/reinstall the app.

Next, we set up the variables we are going to need to create or find a user and store. We’ll pull this information from the auth variable that OmniAuth made for us.

We’ll look up the store by its store_hash to see if we need to create it (new install) or if it already exists (user has installed the app in the past).

Then comes the user. Again, we look to see if one exists and, if not, we create it.

Finally, we set up a session and redirect to the homepage!

def show

def show
# Decode payload
payload = parse_signed_payload
return render_error('[load] Invalid payload signature!') unless payload

email = payload[:user][:email]
name = payload[:user][:name]
bc_id = payload[:uid]
store_hash = payload[:store_hash]

# Lookup store
store = Store.where(store_hash: store_hash).first
return render_error("[load] Store not found!") unless store

# Find/create user
user = User.where(email: email).first_or_create do |user|
user.bc_id = bc_id
user.name = name
user.save!
end
return render_error('[load] User not found!') unless user

# Login and redirect to home page
logger.info "[load] Loading app for user '#{email}' on store '#{store_hash}'"
session[:store_id] = store.id
session[:user_id] = user.id

redirect_to '/'
end

First, we decode the payload that is sent to us from BigCommerce. We do this using the parse_signed_payload method with is borrowed directly from the BigCommerce example Ruby app. If the payload is nil, we render an error.

The rest is similar to the flow in new. We set the variables we need to locate the user and store, set the session, and redirect to the homepage.

In this show action, though, we are going to find the store or return an error because if a store is hitting this controller action then it needs to have already been created via the install/ new flow. No store creation should be happening here.

def destroy

def destroy
# Decode payload
payload = parse_signed_payload
return render_error('[destroy] Invalid payload signature!') unless payload

email = payload[:user][:email]
name = payload[:user][:name]
bc_id = payload[:uid]
store_hash = payload[:store_hash]

# Lookup store
store = Store.where(store_hash: store_hash).first
return render_error("[destroy] Store not found!") unless store

# Find/create user
user = User.where(email: email).first_or_create do |user|
user.bc_id = bc_id
user.name = name
user.save!
end
return render_error('[destroy] User not found!') unless user

logger.info "[destroy] Uninstalling app for store '#{store_hash}'"
store.is_installed = false
logger.info "[destroy] Removing access_token from store '#{store_hash}'"
store.access_token = 'uninstalled'
store.save!

session.clear

render json: "[destroy] App uninstalled from store '#{store_hash}'"
end

Destroy is similar to show, in that we decode the payload first, then set our variables for finding a store and user.

Next, we set the is_installed field to false. You could also delete the store, but I prefer this approach of “soft deleting” instead of trashing data.

Then we need to get rid of the access_token for the store. Again, I like to overwrite it in the database instead of erasing the store.

Finally, we clear the session and render a json response confirmation.

def remove

def remove
render json: {}, status: :no_content
end

I’ll leave this one up to you. For this example, I’m just rendering a json response with status no_content.

You can delete the user, add a new field to mark them as active/inactive, whatever floats your boat!

def failure

def failure
render json: "Your authentication flow has failed with the error: #{params[:message]}"
end

This is to support one of the OmniAuth routes. If authentication fails, it redirects here and we render the error message to the user in json.

Helper methods

private

# Verify given signed_payload string and return the data if valid.
def parse_signed_payload
signed_payload = params[:signed_payload]
message_parts = signed_payload.split('.')

encoded_json_payload = message_parts[0]
encoded_hmac_signature = message_parts[1]

payload = Base64.decode64(encoded_json_payload)
provided_signature = Base64.decode64(encoded_hmac_signature)

expected_signature = sign_payload(ENV['BC_CLIENT_SECRET'], payload)

if secure_compare(expected_signature, provided_signature)
return JSON.parse(payload, symbolize_names: true)
end

nil
end

# Sign payload string using HMAC-SHA256 with given secret
def sign_payload(secret, payload)
OpenSSL::HMAC::hexdigest('sha256', secret, payload)
end

# Time consistent string comparison. Most library implementations
# will fail fast allowing timing attacks.
def secure_compare(a, b)
return false if a.blank? || b.blank? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"

res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end

def render_error(e)
logger.warn "ERROR: #{e}"
@error = e
@again = "Please try reloading or reinstalling the app."
render json: [@error, @again]
end

def logger
Rails.logger
end

As mentioned, we have a few helper methods that we reference throughout our controller. Here they are.

First, we decode the payload that is sent to us from BigCommerce using the parse_signed_payload method with is borrowed directly from the BigCommerce example Ruby app.

Next, sign_payload and secure_compare are methods supporting the parse_signed_payload method.

Finally, render_error and logger are the helper methods which allow us to respond to error situations in our controller with default messaging.

11. Test installing our app

We’re at the point now where we have an app that authenticates with BigCommerce and redirects to our homepage. We can test out the flow by installing our app on our BigCommerce store!

Start by opening two terminal windows.

In one, make sure the Rails server is running for your app.

bin/rails server

In the other, start up the ngrok service using your static domain and exposing your local port of 3000. Remember, your domain will be different, but here’s what the command looks like for me:

ngrok http --domain=allowed-uniquely-mouse.ngrok-free.app 3000

Now that your app is running and ngrok is proxying traffic to your app, you’re ready to go install it on your store!

Log into your BigCommerce store control panel and go to Apps > My Apps in the left hand navigation. Then click on My Draft Apps along the top.

Look at that pretty app we set up!

Click on your app, then click on Install.

You’ll be presented with a screen that explains which permissions you are requesting access to. Review and click Confirm.

Finally, our auth handshake should finish and we should be redirected to our app’s homepage!

If you check your Rails logs, you’ll notice the GET request to /auth/bigcommerce/callback? that comes from clicking the install button in BigCommerce. It contains an account_uuid, a one-time code, a context, and a scope. All information that the omniauth gem uses to complete our authentication handshake.

If you look below that, you’ll see the database calls where we look to see if the store exists in our database and either create or pull that record. Same thing with the User record.

Finally, you can see the redirect to the root path which brings us to our home/index.html.erb template.

Congratulations!

You’ve set up your first app using Rails which connects to BigCommerce using OAuth. That makes you pretty cool!

--

--

John Hebron

Developer (Ruby, Rails). Entrepreneur/small business owner. Tinkerer/creator. Advocate/fiercely passionate human being.