Building an OAuth App for BigCommerce using Rails | Part 2: Creating a Rails 7 App and Connecting to BigCommerce
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
- Install a new Rails 7.0.7.2 app locally
- Set a home page for our Rails app
- Update our Rails app to work with
ngrok
- Edit our
gemfile
/ Add new gems - Set up our local environment variables
- Update the app settings in the BigCommerce Developer Center
- Update our Application Controller for BigCommerce API requests
- Create a
user
andstore
model and table - Set up the `omniauth-bigcommerce` gem
- Create our authentication routes and controller
- 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.
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
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 home
controller. 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-rails
gem. 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:
- In the development and test environments, it will install
dotenv-rails
,which will allow us to use and read from a.env
file - It will install the gems
bigcommerce
andomniauth-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
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!