Authenticate server-side rendered embedded apps using Rails and Turbolinks
Turbolinks is a JavaScript library that makes your app behave as if it were a single-page app.
If you have a multi-page server-side rendered (SSR) app and you want to use session token-based authentication, then you can still use session tokens by converting your app to use Turbolinks. You can do this even if you're unsure or not yet ready to convert your app to a single-page app.
This tutorial shows you how to convert your multi-page SSR app to use session token-based authentication with Turbolinks.
Requirements
Your app must meet the following requirements to complete this tutorial:
- Your app has the Shopify App gem version 17.0.5 or higher installed.
- Your app is enabled to use JSON Web Token (JWT) authentication and session tokens.
- You're familiar with the concept of session tokens and how to use them to authenticate an embedded app.
The Shopify App gem version 17.0.5 and higher creates a JWT-enabled app by default when you run this terminal command:
$ rails generate shopify_app
Conversion pattern
To use session tokens with your multi-page app using Turbolinks, we suggest implementing the recommended conversion pattern:
Create an unauthenticated controller that renders a splash page when a user visits your app.
The splash page communicates to users that your app is loading.
Use the splash page to do the following:
- Create an App Bridge instance.
- Retrieve and cache a session token within your app client.
- Install event listeners to set an
"Authorization": "Bearer <session token>"
request header on the following events:turbolinks:request-start
turbolinks:render
Install a timed event that continues to retrieve and cache session tokens every two seconds.
This ensures that your session tokens are always valid.
Use Turbolinks to navigate to your app's authenticated home page or resource.
Enable Turbolinks on your app
Use the following steps to enable Turbolinks on your app.
Steps
- Add the
turbolinks
gem to your Gemfile:
gem 'turbolinks', '~> 5'
Run
bundle install
.Add the
turbolinks
package to your application.
$ yarn add turbolinks
- If your app uses webpack to manage its manifest files, then add the following line to
app/javascript/packs/application.js
:
require("turbolinks").start();
Create a splash page
Your splash page is used to indicate that your app has begun to fetch a session token. When your app has the token, it should navigate the user to the main view. The main view might contain protected or authenticated resources.
Steps
- Create a
SplashPageController
along with a default index action and view:
$ rails generate controller splash_page index
- Make
splash_page#index
the default root route for your app. Update the following code in yourroutes.rb
file:
Rails.application.routes.draw do
root to: 'splash_page#index'
...
end
- Indicate a loading status in your splash page index view. Update
app/views/splash_page/index.html.erb
to match the following code:
<p>Loading...</p>
- Make the
SplashPageController
behave as the default embedded appHomeController
by updatingapp/controllers/splash_page_controller.rb
to match the following code:
class SplashPageController < ApplicationController
include ShopifyApp::EmbeddedApp
include ShopifyApp::RequireKnownShop
def index
@shop_origin = current_shopify_domain
end
end
- Protect the default
HomeController
by inheritingAuthenticatedController
. Updatehome_controller.rb
to match the following code:
class HomeController < AuthenticatedController
def index
@shop_origin = current_shopify_domain
end
end
Fetch and store session tokens
When users visit the app for the first time, you can use Javascript to complete the following tasks on the splash page:
Create an App Bridge instance.
Fetch a session token and cache it.
Install event listeners on the
turbolinks:request-start
andturbolinks:render
events to add anAuthorization
request header.Install event listeners to add an
Authorization
request header on the following events:turbolinks:request-start
turbolinks:render
Use Turbolinks to navigate to the
HomeController
.
Steps
- Add the following
load_path
parameter toapp/views/layouts/embedded_app.html.erb
. This parameter is used by Turbolinks to navigate back to this app when a session token has been fetched. In the following example, you're navigating tohome_path
by default:
...
<%= 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,
...
} ) %>
...
- Import the library method
getSessionToken
fromapp-bridge-utils
inapp/javascript/shopify_app/shopify_app.js
:
import { getSessionToken } from "@shopify/app-bridge-utils";
- In
shopify_app.js
, include the following methods that fetch and store a session token every two seconds:
const SESSION_TOKEN_REFRESH_INTERVAL = 2000; // Request a new token every 2s
async function retrieveToken(app) {
window.sessionToken = await getSessionToken(app);
}
function keepRetrievingToken(app) {
setInterval(() => {
retrieveToken(app);
}, SESSION_TOKEN_REFRESH_INTERVAL);
}
- In
shopify_app.js
, also include the following helper method and replace'/home'
with your app'shome_path
. The helper method navigates your app using Turbolinks. It does this by determining whether the navigation is made on the initial load of your app.
function redirectThroughTurbolinks(isInitialRedirect = false) {
var data = document.getElementById("shopify-app-init").dataset;
var validLoadPath = data && data.loadPath;
var shouldRedirect = false;
switch (isInitialRedirect) {
case true:
shouldRedirect = validLoadPath;
break;
case false:
shouldRedirect = validLoadPath && data.loadPath !== "/home";
break;
}
if (shouldRedirect) Turbolinks.visit(data.loadPath);
}
- In
shopify_app.js
, add event listeners to theturbolinks:request-start
andturbolinks:render
events. Set an"Authorization": "Bearer <session token>"
header during these events:
document.addEventListener("turbolinks:request-start", function (event) {
var xhr = event.data.xhr;
xhr.setRequestHeader("Authorization", "Bearer " + window.sessionToken);
});
document.addEventListener("turbolinks:render", function () {
$("form, a[data-method=delete]").on("ajax:beforeSend", function (event) {
const xhr = event.detail[0];
xhr.setRequestHeader("Authorization", "Bearer " + window.sessionToken);
});
});
- In
shopify_app.js
, edit theDOMContentLoaded
event listener to add the following instructions:
document.addEventListener("DOMContentLoaded", async () => {
var data = document.getElementById("shopify-app-init").dataset;
var AppBridge = window["app-bridge"];
var createApp = AppBridge.default;
window.app = createApp({
apiKey: data.apiKey,
shopOrigin: data.shopOrigin,
});
var actions = AppBridge.actions;
var 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
var isInitialRedirect = true;
redirectThroughTurbolinks(isInitialRedirect);
document.addEventListener("turbolinks:load", function (event) {
redirectThroughTurbolinks();
});
});
After a session token is retrieved, Turbolinks.visit(data.loadPath)
visits the load_path
param defined in embedded_app.html.erb
.
Your app continues to retrieve session tokens every two seconds.
Request authenticated resources
When a user visits your app, they should now briefly see a loading screen before they're taken to the Home view of your app. The Home view is authenticated by the HomeController
.
For demo purposes, we have created two additional authenticated controllers: ProductsController
and WidgetsController
. The following steps describe how to create the ProductsController
and add a navigation link to the Home view.
Steps
- Generate a
ProductsController
using the Rails generator:
$ rails generate controller products index
- Add authentication to the
ProductsController
by inheritingAuthenticatedController
:
class ProductsController < AuthenticatedController
def index
@products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
end
end
- Create a view for the
ProductsController
. Updateapp/views/products/index.html.erb
to match the following code:
<%= link_to 'Back', home_path %>
<h2>Products</h2>
<ul>
<% @products.each do |product| %>
<li><%= link_to product.title, "https://#{@current_shopify_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
<% end %>
</ul>
- Add the
ShopifyApp::EnsureAuthenticatedLinks
concern toAuthenticatedController
to authenticate pages of your app that are deep-linked. This concern is available in Shopify App version 17.0.5:
class AuthenticatedController < ApplicationController
include ShopifyApp::EnsureAuthenticatedLinks
include ShopifyApp::Authenticated
end
- Update
app/views/home/index.html.erb
to include a link to the product index view:
<%= link_to 'Products', products_path(shop: @shop_origin) %>
Your app can now access the authenticated ProductsController
from the HomeController
using session tokens.
Mark shop records as uninstalled
To ensure OAuth continues to work with session tokens, your app must update its shop records in the event a shop uninstalls your app. The app can receive notifications of these events by subscribing to the app/uninstalled
webhook. For more information, refer to Mark a shop record as uninstalled using webhooks.
Resources
Sample server-side rendered Rails app converted using Turbolinks
Next steps
- Make authenticated requests using Axios.
- Use helper functions to fetch a session token from App Bridge and include them in requests being made to the app backend.
- Learn how to build a Shopify app with Rails 6, React, and App Bridge authentication.