Last active
January 6, 2024 19:28
-
-
Save jduff/bedc2eee90fc168a8707838325b04122 to your computer and use it in GitHub Desktop.
Basic Shopify OAuth implementation in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Shopify::AuthsController < ApplicationController | |
NONCE_LENGTH = 15 | |
AUTH_COOKIE_NAME = "_shopify_auth_session" | |
before_action :verify_request_hmac, only: :callback | |
before_action :verify_request_state, only: :callback | |
def new | |
# TODO: Check if we have a token already or if scopes have changed | |
if params[:embedded] | |
headers.except!("X-Frame-Options") | |
# redirect out of iframe when embedded | |
full_page_redirect_to(new_shopify_auth_url(shop: params[:shop])) | |
else | |
# TODO: validate params[:shop] | |
# there are some different formats to take into account https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/utils.rb#L6 | |
# might need to look at referrer for embedded https://github.com/search?q=repo%3AShopify%2Fshopify_app%20referer_sanitized_shop_name&type=code | |
state = SecureRandom.alphanumeric(NONCE_LENGTH) | |
cookies.encrypted[AUTH_COOKIE_NAME] = { | |
expires: Time.now + 60, | |
secure: true, | |
http_only: true, | |
value: state, | |
} | |
query = { | |
client_id: ENV['SHOPIFY_APP_CLIENT_ID'], | |
scope: "write_orders", | |
redirect_uri: callback_shopify_auths_url(host: ENV['APP_HOSTNAME'], protocol: "https"), | |
"grant_options[]" => "", | |
state: state, | |
} | |
# TODO: use ActionDispatch::Http::URL.url_for to build urls, script_name: for path | |
redirect_to("https://#{params[:shop]}/admin/oauth/authorize?#{query.to_query}", allow_other_host: true) | |
end | |
end | |
def callback | |
query = { | |
client_id: ENV['SHOPIFY_APP_CLIENT_ID'], | |
client_secret: ENV['SHOPIFY_APP_CLIENT_SECRET'], | |
code: params[:code], | |
} | |
response = HTTParty.post("https://#{params[:shop]}/admin/oauth/access_token", query: query) | |
# TODO: store response["access_token"] and response["scope"] | |
# TODO: check if this app should be embedded or not | |
if params[:embedded] != "1" | |
decoded_host = Base64.decode64(params[:host]) | |
redirect_to("https://#{decoded_host}/apps/#{ENV['SHOPIFY_APP_CLIENT_ID']}", allow_other_host: true) | |
else | |
redirect_to(root_path(params: { shop: session.shop, host: params[:host] })) | |
end | |
end | |
private | |
def full_page_redirect_to(url) | |
render("shopify/shared/redirect", layout: false, locals: { url: url }) | |
end | |
def verify_request_state | |
state = cookies.encrypted[AUTH_COOKIE_NAME] | |
cookies.delete(AUTH_COOKIE_NAME) | |
raise "Invalid Request" unless params[:state] == state | |
end | |
def verify_request_hmac | |
request_params = params.except(:controller, :action, :hmac).permit! | |
raise "Invalid Request" unless valid_request?(request_params, params[:hmac], ENV['SHOPIFY_APP_CLIENT_SECRET']) | |
end | |
def valid_request?(params, hmac, secret) | |
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, params.to_param) | |
ActiveSupport::SecurityUtils.secure_compare(digest, hmac) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Redirecting...</title> | |
<meta name="viewport" content="width=device-width,initial-scale=1"> | |
<%= csrf_meta_tags %> | |
<%= csp_meta_tag %> | |
<%= tag("meta", name: "shopify-api-key", content: ENV['SHOPIFY_APP_CLIENT_ID']) %> | |
<%= tag("meta", name: "shopify-host", content: params[:host]) %> | |
<%= javascript_importmap_tags %> | |
</head> | |
<body> | |
<%= content_tag :div, nil, data: { controller: "shopify--redirect", shopify__redirect_action_value: "REMOTE", shopify__redirect_immediate_value: true }, href: url %> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Controller } from "@hotwired/stimulus" | |
import { Redirect } from "@shopify/app-bridge/actions"; | |
// Connects to data-controller="shopify--redirect" | |
export default class RedirectController extends Controller { | |
static Action = { | |
ADMIN_PATH: Redirect.Action.ADMIN_PATH, | |
ADMIN_SECTION: Redirect.Action.ADMIN_SECTION, | |
APP: Redirect.Action.APP, | |
REMOTE: Redirect.Action.REMOTE | |
}; | |
static values = { | |
action: "ADMIN_PATH", | |
immediate: false, | |
} | |
connect() { | |
console.assert(this.actionValue, Object.keys(RedirectController.Action)); | |
this.shopifyApiKey = document.head.querySelector(`meta[name="shopify-api-key"]`)?.getAttribute('content'); | |
this.shopifyHost = document.head.querySelector(`meta[name="shopify-host"]`)?.getAttribute('content'); | |
this.shopifyApp = createApp({ | |
apiKey: this.shopifyApiKey, | |
host: this.shopifyHost, | |
forceRedirect: true, | |
}); | |
this.redirect = Redirect.create(this.shopifyApp) | |
if(this.immediateValue) { | |
this.dispatch(); | |
} | |
} | |
dispatch(event) { | |
event?.preventDefault(); | |
this.redirect.dispatch(this.action, { | |
url: this.url, | |
newContext: this.newContext, | |
}); | |
} | |
get action() { | |
return RedirectController.Action[this.actionValue] ?? Redirect.Action.ADMIN_PATH; | |
} | |
get url() { | |
return this.element.getAttribute('href'); | |
} | |
get newContext() { | |
return this.element.getAttribute('target') === '_blank'; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment