Navigating Session Token Authentication in Shopify for Embedded, Server-Side Rendered Rails Apps
Tags: rails, shopify, authentication, tutorial
Published: 2022-10-05 19:47:33

Recently we built an embedded Shopify application for one of our clients. The UI was relatively straightforward, and in an effort to keep the code simple we elected to create a server-side rendered (SSR) Rails app and use [Hotwire](https://hotwired.dev/) ([Turbo](https://turbo.hotwired.dev/) and [Stimulus](https://stimulus.hotwired.dev/), specifically) as needed for the front end.

Part of building a Shopify application means going through a rigorous [review and approval process](https://shopify.dev/apps/store/review). The app is submitted to Shopify and the Shopify team goes through it with a fine-tooth comb in an attempt to validate functionality and find any bugs that would be frustrating for merchants - free beta testing! One of the requirements for newly submitted Shopify applications is that they use [session token authentication with AppBridge2.0](https://shopify.dev/apps/auth/oauth/session-tokens). Fortunately, we were using the `shopify_app` gem, a popular Rails engine for building Shopify apps that is maintained by Shopify. This library does a lot of heavy lifting, specifically around boilerplate authentication code. We thought that it would Just Work<sup>TM</sup> but we were wrong.😱

This article serves to be a comprehensive guide to navigating Shopify’s session token authentication for embedded, server-side rendered Rails apps. We hope to expose some common pitfalls and provide clear and concrete examples to guide you through the process. Let’s hit the road, shall we?

## Missing Our Exit

We submitted our application to Shopify and it was returned with a few small items and one rather surprising one: “You haven’t implemented OAuth correctly”. We've implemented many OAuth integrations and many Shopify integrations on past projects, so what was the issue here? We had run into some minor issues in the past that seemed authentication-related, namely:

- Authenticated content flashing on the screen before redirection to a login screen when acting as an unauthenticated merchant
- Errors around a missing or incorrectly formatted `host` parameter
- An error stating that there was no app at a given location during an uninstall/reinstall cycle

We had resolved these issues and were under the impression that they were just one-off bugs. We trusted that the `shopify_app` gem had correctly implemented session token authentication and besides, there were no errors in the console or server logs, and we were able to successfully authenticate and install the app on our development stores without issue. The reviewer was kind enough to provide a screen recording of the OAuth-related error and after digging through our logs, it became clear that we were seeing widespread 5xx errors for _Invalid or missing Cross-Site Request Forgery (CSRF) token_. 

Uhh…what? None of the developers had experienced this issue locally or during QA cycles.

We attempted to replicate this using different combinations of stores and app environments to no avail. Finally, a teammate remembered reading that Shopify testing is performed in an incognito browser window with cookies disabled. When we mirrored that setup, BOOM, there they were - the _Invalid or missing CSRF token_ errors. This _does_ make sense though because the [Rails CSRF protection strategy](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) uses cookies, which were now disabled. After some digging we discovered this bit in the Shopify [documentation](https://shopify.dev/apps/auth/oauth/session-tokens/getting-started):

> Without third-party cookies, setting Cross-Site Request Forgery (CSRF) tokens in a cookie might not be possible. The session token serves as an alternative to CSRF tokens, because you can trust that the session token has been issued by Shopify to your app frontend.

This makes sense from a modern Shopify perspective as well. Session Token authentication and the [AppBridge](https://shopify.dev/apps/tools/app-bridge) library were implemented and are now enforced as a result of browsers’ increasing restrictions on the use of cookies in iframes.

But … we were using AppBridge. So why wasn’t the session token being used as an alternative as described in the documentation? After hours of reading through Shopify Community Forum Discussion posts and poring over an overwhelming expanse of documentation at Shopify.dev, we uncovered [this section](https://shopify.dev/apps/auth/oauth/session-tokens#limitations-and-considerations) and more specifically, this link:

[Authenticate a server-side rendered embedded app using Rails and Turbolinks](https://shopify.dev/apps/auth/oauth/session-tokens/server-side-rendered-apps).

nestled in the introductory session token documentation. If there was ever a time to use the RTFM expression, it was now. We couldn’t help but wonder why this wasn’t made glaringly obvious, though. This wasn’t the [first run-in we’d had with inadequate Shopify documentation](https://www.thegnar.com/blog/managing-shopify-app-extensions-across-environments). Were we the first people to write a server-side rendered embedded Shopify app with a Rails backend? Surely this wasn’t a novel approach?

## Taking a Detour

We followed the sample app and instructions in the [documentation](https://shopify.dev/apps/auth/oauth/session-tokens/server-side-rendered-apps) as closely as possible. There were a few obvious differences we knew we’d need to contend with, like replacing Turbolinks events with their modern Turbo counterparts and removing the rogue jQuery references throughout the front-end code. Despite following the tutorial to a T, now, we were unable to authenticate **_at all_**. Instead, we saw a confusing combination of white screens and “missing host parameter” errors in the developer console, despite a `host` being set in our `ApplicationController`. 🤦


## Finding an Alternate Route

Feeling a bit discouraged, we did what any good engineering team would do and hit the web looking for an example of someone else who had implemented this using a modern Rails approach. Enter ✨ [`shopify-hotwire-sample`](https://github.com/kirillplatonov/shopify-hotwire-sample) ✨. This repository was synonymous with the sample app provided as a supplement to the Turbolinks-specific tutorial but used Turbo instead. The code actually ended up being a bit _more_ comprehensive than what we needed but it served as an extremely helpful guide in getting session token auth working properly. Given how complex this process was, I want to take you through the process step-by-step to illustrate how we implemented the feature using a combination of the tutorials and sample apps mentioned earlier in the article.

### Create a Splash Page
The splash page is intended to be an unauthenticated page that is rendered when a user visits the app and should indicate that the app is loading. From a technical perspective, the splash page indicates that your app has begun to fetch a session token. After your app receives the token, the user should be directed to the main view (the home page), which may contain authenticated resources.

Let’s add a controller with an `index` action that sets a `@shop_origin` instance variable to the `current_shopify_domain` helper. The helper and the included modules come out of the box with the `shopify_app` gem.

```ruby
# In app/controllers/splash_page_controller.rb

class SplashPageController < ApplicationController
  include ShopifyApp::EmbeddedApp
  include ShopifyApp::RequireKnownShop
  include ShopifyApp::ShopAccessScopesVerification

  def index
    @shop_origin = current_shopify_domain
  end
end
```

Tests are good too. 

```ruby
# In spec/request/splash_page_request_spec.rb

require "rails_helper"

RSpec.describe "SplashPages", type: :request do
  describe "GET /" do
    it "returns http success" do
      get "/"

      expect(response).to have_http_status(:found)
    end
  end
end
```

Make sure the root URL for the app is set to the splash page, and create a new endpoint for the home page. Remember to update any feature specs!

```ruby
# In config/routes.rb

root to: "splash_page#index"
get "/home", to: "home#index", as: :home
```

Create the view for the splash page, and add an indicator to illustrate that the app is loading.

```ruby
<%# In app/views/splash_page/index.html.erb %>

<div class="splash-page__loading">
  <div class="bounce1"></div>
  <div class="bounce2"></div>
  <div class="bounce3"></div>
</div>
```

Style the indicator, because we’re fancy (the styles were [yoinked](https://github.com/kirillplatonov/shopify-hotwire-sample/blob/main/app/views/splash_page/index.html.erb) from the `shopify-hotwire-sample` repo).

```css
/* In app/assets/stylesheets/loading.scss */

.splash-page__loading {
  align-items: center;
  display: flex;
  height: 100vh;
  justify-content: center;
}

.splash-page__loading > div {
  -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
  animation: sk-bouncedelay 1.4s infinite ease-in-out both;
  background-color: #c4cdd5;
  border-radius: 100%;
  display: inline-block;
  height: 18px;
  width: 18px;
}

.splash-page__loading .bounce1 {
  -webkit-animation-delay: -0.32s;
  animation-delay: -0.32s;
}

.splash-page__loading .bounce2 {
  -webkit-animation-delay: -0.16s;
  animation-delay: -0.16s;
}

@-webkit-keyframes sk-bouncedelay {
  0%,
  80%,
  100% {
    -webkit-transform: scale(0);
  }

  40% {
    -webkit-transform: scale(1);
  }
}

@keyframes sk-bouncedelay {
  0%,
  80%,
  100% {
    -webkit-transform: scale(0);
    transform: scale(0);
  }

  40% {
    -webkit-transform: scale(1);
    transform: scale(1);
  }
}
```

### Protect the Controllers

We no longer want our controllers to inherit from `ApplicationController` because we should not be able to access them unless we are authenticated. We can handle this by making the controllers that require authentication inherit from the `AuthenticatedController` (which comes from `shopify_app` as boilerplate).

For reference, our `ApplicationController` looks like this and does a number of things, namely setting up some instance variables for `@shop`, `@shop_origin`, and `@host` that will be globally available in our controllers and views, as well as checking the installation of a shop and setting up a [content security policy for iframe protection](https://shopify.dev/apps/store/security/iframe-protection).

```ruby
# In app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :set_shopify_variables
  before_action :set_shop
  before_action :check_installation
  after_action :set_csp_header

  private

  def set_shop
    @shop = Shop.find_by(shopify_domain: @shop_origin)
  end

  def set_shopify_variables
    @shop_origin = current_shopify_domain
    @host = host
  end

  def shopify_params
    params.permit(:hmac, :host, :session, :shop, :locale, :authenticity_token)
  end

  def host
    params[:host]
  end

  def check_installation
    return if Rails.env.test? || @shop.nil?

    ShopifyApiSession.create(shop: @shop)
    begin
      ShopifyAPI::Shop.current
    rescue ActiveResource::UnauthorizedAccess
      redirect_to "#{ENV['HOST']}/login?shop=#{@shop.shopify_domain}"
    end
  end

  def set_csp_header
    response.set_header(
      "Content-Security-Policy",
      "frame-ancestors https://admin.shopify.com https://#{current_shopify_domain};",
    )
  end
end
```

### Skip Forgery Protection for Controllers That Make External API Calls 

Remember that CSRF issue I mentioned earlier? Well, in this app we are making one type of API call that doesn’t need to be protected as a part of Shopify and can live outside of Shopify’s authentication system. This may not apply to you, but it is required in our application. 

```ruby
# In app/controllers/api_controller.rb

class ApiController < ActionController::Base
  skip_forgery_protection
  
  def create
    # API stuff
  end
end
```

### Add Middleware to Properly Set and Encode the `host`

One neat thing about this feature is that we get to write and test custom middleware! In the event that we are missing the host key in params during an authentication cycle, this piece of middleware will encode the `host` in the correct format and set the value of `host` in the request params.

```ruby
# In app/middleware/app_bridge_middleware.rb

class AppBridgeMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)

    if request.params.has_key?("shop") && !request.params.has_key?("host")
      shop = request.params["shop"]
      host = Base64.urlsafe_encode64("#{shop}/admin", padding: false)
      request.update_param("host", host)
    end

    @app.call(env)
  end
end
```

We need to ensure that the middleware is available in our application.

```ruby
# In config/application.rb

require_relative "../app/middleware/app_bridge_middleware"

# ...

module YourApp
  class Application < Rails::Application

    # ...

    config.middleware.use AppBridgeMiddleware
  end
end
```

And we need to test it, of course.

```ruby
# In spec/middleware/app_bridge_middleware_spec.rb

require "rails_helper"

RSpec.describe AppBridgeMiddleware, type: :request do
  it "adds missing host params" do
    shop_domain = "foo.myshopify.com"
    base64_encoded_host = Base64.urlsafe_encode64("foo.myshopify.com/admin", padding: false)

    get "/?shop=#{shop_domain}"

    expect(request.params["host"])
      .to eq(base64_encoded_host)
  end
end
```

### Update the Embedded App Layout

Before we start writing JavaScript, we need to update the `app/views/layouts/embedded_app.html.erb` file to pass along a `load_path` data attribute that will be used by Turbo to navigate back to this app when a session token has been retrieved. In our example, we’re navigating to the `home_path`.

```ruby
<%# In app/views/layouts/embedded_app.html.erb %>


<%# … %>

<%= content_tag(:div, nil, id: 'shopify-app-init', data: {
  api_key: ShopifyApp.configuration.api_key,
  shop_origin: @shop_origin || (@current_shopify_session.domain if     @current_shopify_session),
  load_path: params[:return_to] || home_path, # Add this line
  host: @host,
  debug: Rails.env.development?,
})%>
```

### Fetch and Store Your Session Tokens with JavaScript

Now, we can write some JavaScript. In essence, what we’re doing is as follows:

- Creating a Shopify AppBridge instance
- Retrieving a session token and caching it
- Installing event listeners on Turbo events to add an Authorization request header
- Using Turbo to navigate to the `HomeController`

First let’s grab the required packages. We’re using yarn.

```bash
$ yarn add @shopify/app-bridge-utils
```

Now we can write some JavaScript to perform the steps outlined above. Note that this JavaScript was taken largely in part from the two sample repos linked above. Some of it was boilerplate from `shopify_app`, and some of it is refactored by us.

```js
// In app/javascript/shopify_app/shopify_app.js

import { getSessionToken } from '@shopify/app-bridge-utils'

const SESSION_TOKEN_REFRESH_INTERVAL = 2000 // Request a new token every 2s to ensure your tokens are always valid

document.addEventListener('turbo:before-fetch-request', function (event) {
  event.detail.fetchOptions.headers['Authorization'] = `Bearer ${window.sessionToken}`
})

document.addEventListener('turbo:render', function () {
  document.querySelectorAll('form, a[data-method=delete]').forEach(element => {
    element.addEventListener('ajax:beforeSend', event => {
      event.detail.fetchOptions.headers['Authorization'] = `Bearer ${window.sessionToken}`
    })
  })
})

document.addEventListener('DOMContentLoaded', async () => {
  const data = document.getElementById('shopify-app-init').dataset
  const AppBridge = window['app-bridge']
  const createApp = AppBridge.default

  window.app = createApp({
    apiKey: data.apiKey,
    host: data.host,
    forceDirect: true,
  })

  const actions = AppBridge.actions
  const TitleBar = actions.TitleBar
  TitleBar.create(app, {
    title: data.page,
  })

  // Wait for a session token before trying to load an authenticated page
  await retrieveToken(app)

  // Keep retrieving a session token periodically
  keepRetrievingToken(app)

  // Redirect to the requested page when DOM loads
  const isInitialRedirect = true
  redirectThroughTurbo(isInitialRedirect)

  document.addEventListener('turbo:load', function (event) {
    redirectThroughTurbo()
  })
})

// Helper functions
function redirectThroughTurbo(isInitialRedirect = false) {
  const data = document.getElementById('shopify-app-init').dataset
  const validLoadPath = data && data.loadPath

  const shouldRedirect = isInitialRedirect
    ? validLoadPath
    : validLoadPath && data.loadPath !== '/home'

  if (shouldRedirect) Turbo.visit(data.loadPath)
}

async function retrieveToken(app) {
  window.sessionToken = await getSessionToken(app)
}

function keepRetrievingToken(app) {
  setInterval(() => {
    retrieveToken(app)
  }, SESSION_TOKEN_REFRESH_INTERVAL)
}
```

### Make Sure Your Shop Records Stay Up-To-Date

The [final steps in the Shopify tutorial](https://shopify.dev/apps/auth/oauth/session-tokens/server-side-rendered-apps#step-6-make-sure-shop-records-are-updated) felt a bit disparate in nature. The instructions suggest that `shop` records be removed from the database when the corresponding Shopify shop uninstalls our application. There isn't a reason provided in the documentation as to why this is explicitly necessary and frankly, we were never able to uncover why this is required for authentication purposes. From the docs:

> To ensure OAuth continues to work with session tokens, your app must update its shop records when a shop uninstalls your app…

However, it _does_ seem to be a reasonable request, and likely a best practice, to eliminate data after a merchant has elected to uninstall the Shopify application. Let’s make sure their data isn’t hanging around.

#### Register the app/uninstalled Webhook For New and Existing Shops

Shopify provides a [number of webhooks](https://shopify.dev/apps/webhooks) that we can subscribe to at the shop-level. One of them is the `app/uninstalled` webhook, which will allow us to receive a request any time a merchant uninstalls the Shopify app from their shop. The `shopify_app` gem provides a lot of the webhook endpoint setup behind the scenes, the details of which are outside the scope of this article but you can learn more [here](https://github.com/Shopify/shopify_app/blob/main/docs/shopify_app/webhooks.md) if you’d like. What _is_ relevant is that we can configure webhooks and add a background job that is named based on the webhook topic, which will automatically be invoked when the webhook is received. 

```ruby
# In config/initializers/shopify_app.rb

ShopifyApp.configure do |config|
  # ...
  
  # This assumes you have an environment variable named HOST
  config.webhooks = [
    { topic: "app/uninstalled", address: "#{ENV['HOST']}/webhooks/app_uninstalled" },
  ]
end
```

We’ve added our new webhook, easy as pie. Moving forward, each merchant that installs our Shopify application on their store will be automatically subscribed to this webhook. However, existing stores will not be subscribed so we’ll need to handle those cases. Let’s add a tested background job that we can invoke from the console to update existing `shop` records. To do this, we can leverage the [`ShopifyApp::WebhooksManagerJob`](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/managers/webhooks_manager.rb) that is provided by the `shopify_app` gem.

```ruby
# In app/jobs/register_webhooks_for_shops_job.rb

class RegisterWebhooksForShopsJob < ApplicationJob
  def perform
    Shop.find_each do |shop|
      ShopifyApp::WebhooksManagerJob.perform_now(
        shop_domain: shop.shopify_domain,
        shop_token: shop.shopify_token,
        webhooks: ShopifyApp.configuration.webhooks,
      )
    end
  end
end
```

```ruby
# In spec/jobs/register_webhooks_for_shops_job_spec.rb

require "rails_helper"

describe RegisterWebhooksForShopsJob, type: :job do
  describe "#perform" do
    it "performs the ShopifyApp::WebhooksManagerJob for each shop" do
      shop_one, shop_two = create_pair(:shop)
      webhooks = ShopifyApp.configuration.webhooks

      allow(ShopifyApp::WebhooksManagerJob).to receive(:perform_now)
      allow(ShopifyApp::WebhooksManagerJob).to receive(:perform_now)

      RegisterWebhooksForShopsJob.new.perform

      expect(ShopifyApp::WebhooksManagerJob).to have_received(:perform_now)
        .with(
          shop_domain: shop_one.shopify_domain,
          shop_token: shop_one.shopify_token,
          webhooks:,
        )
      expect(ShopifyApp::WebhooksManagerJob).to have_received(:perform_now)
        .with(
          shop_domain: shop_two.shopify_domain,
          shop_token: shop_two.shopify_token,
          webhooks:,
        )
    end
  end
end
```

Now, we can open up our `rails console` and run `RegisterWebhooksForShopsJob.perform_later` to ensure the new webhook gets registered for all existing `shop`s. Make sure your Sidekiq server is running!

#### Destroy Uninstalled Shops

The final step in the Shopify tutorial is to add the background job that is responsible for destroying the `shop` after the Shopify app has been uninstalled. It is a **requirement** that the name of this job reflects the webhook topic (`app/uninstalled`) since this is how the `shopify_app` gem knows what job to trigger when a webhook is received. The `params` that the `#perform` method receives are the parameters included in the webhook request.

```ruby
# In app/jobs/app_uninstalled_job.rb

class AppUninstalledJob < ApplicationJob
  def perform(params)
    shop = Shop.find_by(shopify_domain: params[:shop_domain])

    shop.destroy! if shop
  end
end
```

```ruby
# In spec/jobs/app_uninstalled_job_spec.rb

require "rails_helper"

describe AppUninstalledJob, type: :job do
  describe "#perform" do
    it "destroys a shop" do
      shop = create(:shop)
      params = { shop_domain: shop.shopify_domain, webhook: {} }

      expect { described_class.new.perform(params) }.to change(Shop, :count).from(1).to(0)
    end
  end
end
```

At this point we were able to authenticate in the browser and could see in the network tab that the session token was being fetched every two seconds as desired. We could also see our fun loading animation on a hard refresh. So… we’re done right? Not quite.

## More Roadblocks

If you’re anything like us, you write a lot of tests. In this app, we had a fair amount of feature specs and despite this implementation working just fine in the browser, **all** of our feature specs were failing. What gives? Well, our embedded Shopify app is intended to be rendered in an iframe, which doesn’t jibe well with acceptance tests. Fortunately, the [engineer](https://github.com/kirillplatonov) who created `shopify-hotwire-sample` also wrote a great article on [testing Shopify apps in Rails](https://kirillplatonov.com/posts/testing-shopify-apps-in-rails/). 

The approach used by the author, and the one we ultimately adopted, involves escaping the iframe by interfering with how AppBridge redirects in a test environment and fallback to cookie authentication. We need to make a few more updates to get our test suite back to green so let’s get to it.

### Configure a Flag in Your Environment Files

Let’s add a custom boolean configuration variable called `force_iframe` and set it to true in all environments _**except**_ our test environment. We don’t want to escape the iframe in development or production. 

```ruby
# In config/environment/test.rb

Rails.application.configure do
  # ...
  config.force_iframe = false
end

# In config/environment/development.rb

Rails.application.configure do
  # ...
  config.force_iframe = true
end

# In config/environment/production.rb

Rails.application.configure do
  # ...
  config.force_iframe = true
end
```

### Update the Embedded App Layout (Again)

This time, we’ll add a data attribute that passes the value of the environment’s  `force_iframe` configuration variable to the front end.

```ruby
<%# In app/views/layouts/embedded_app.html.erb %>

<%= content_tag(:div, nil, id: 'shopify-app-init', data: {
        api_key: ShopifyApp.configuration.api_key,
        shop_origin: @shop_origin || (@current_shopify_session.domain if     @current_shopify_session),
        load_path: params[:return_to] || home_path,
        host: @host,
        debug: Rails.env.development?,
        force_iframe: Rails.configuration.force_iframe.to_s, # Add this line
} ) %>
```

### Guard Session Token Retrieval in the Test Environment

Since we are sending the `force_iframe` data attribute, we can use conditional logic to proceed as normal when the value of `force_iframe` is true, and halt the execution in the case where it is false.

```js
// In app/javascript/shopify_app/shopify_app.js

document.addEventListener('DOMContentLoaded', async () => {
  const data = document.getElementById('shopify-app-init').dataset
  const AppBridge = window['app-bridge']
  const createApp = AppBridge.default
  
  // Add this guard clause here
  if (data.forceIframe === 'false') {
    return
  }

  window.app = createApp({
    apiKey: data.apiKey,
    host: data.host,
    forceDirect: true,
  })
  
  // ...
})
```

### Skip Splash Page Redirection Based on `force_iframe`

Next, we’ll want to prevent the `AuthenticatedController` from redirecting to the splash page unless we are in an environment where `force_iframe` is set to true so we can bypass token retrieval.

```ruby
# In app/controllers/authenticated_controller

class AuthenticatedController < ApplicationController
  # ...

  skip_before_action :redirect_to_splash_page, unless: -> { Rails.configuration.force_iframe }
end
```

### Fallback to Cookies in the Test Environment

We can configure `ShopifyApp` to fallback to cookie authentication using a boolean. We chose to set this boolean based on the value of `Rails.env.test?`, but you could also use the inverse of `Rails.configuration.force_iframe`.

```ruby
# In config/initializers/shopify_app.rb
ShopifyApp.configure do |config|
  # ... 

  config.allow_cookie_authentication = Rails.env.test?
end
```

### Set Up Test Helpers

The final piece in being able to successfully run our feature specs involves adding a helper that will allow us to mock OmniAuth and login as a `shop` in our tests. It’s worth mentioning that you would also need something similar for integration tests, if you have them. More on that under section 3, [here](https://kirillplatonov.com/posts/testing-shopify-apps-in-rails/). 

```ruby
# In spec/support/shopify_feature_helpers.rb

module ShopifyFeatureHelpers
  def login(shop)
    OmniAuth.config.test_mode = true
    OmniAuth.config.add_mock(
      :shopify,
      provider: :shopify,
      uid: shop.shopify_domain,
      credentials: { token: shop.shopify_token },
      extra: {
        scope: ENV.fetch("SCOPES", ""),
      },
    )
    OmniAuth.config.allowed_request_methods = [:post, :get]
    OmniAuth.config.silence_get_warning = true

    Rails.application.env_config["jwt.shopify_domain"] = shop.shopify_domain
    visit "/auth/shopify"
  end

  def clear_login
    Rails.application.env_config.delete("jwt.shopify_domain")
  end
end

RSpec.configure do |config|
  config.include ShopifyFeatureHelpers, type: :feature
end
```

At this point, the implementation should work in the browser and your feature specs should be back to that sweet, sweet green state.

## Looking in the Rear View
Shopify is a really powerful, full-featured e-commerce platform and it has helped us to create some really incredible applications for our clients. However, there are certain aspects of Shopify development that are challenging. The vast documentation is not always clear and the expectations set forth are not always obvious. Our hope is that this guide has saved you some time on your journey to implement Shopify’s session token authentication for an embedded, server-side rendered Rails application, and that you’re now on your way to a well-earned Shopify app approval. Happy Trails!

_Learn more about how The Gnar [builds Shopify apps](https://www.thegnar.com/software-development/shopify-app-development)._