概要
業務の一環でShopifyのMultipass APIを使いSSO(シングル・サイン・オン)を実装する機会があったので、手順などについてメモ書きしておきたいと思います。
SSO: 単一の資格情報(IDやメールアドレス)で複数のWebサービスにログインできる仕組みの事。
全体的な流れ
- Shopify側でMultipassを有効化しシークレットキーを発行。
- SSO基盤側でシークレットキーをもとに暗号化キーと署名キーを抽出。さらに顧客情報(メールアドレス、ユーザーID、IPアドレス、名前、住所など)を暗号化したトークンを生成し、それらを含めたGETリクエストをShopify側へ送信。(GET: https://ストアのドメイン/account/login/multipass/トークン)
- 認証に成功するとマイページに遷移。
環境
- Shopify Plus
- Docker
- Ruby 2.6
- Rails 6
- MySQL 8
まず大前提として、Shopify Plusプランに加入している必要があるのでご注意ください。(Multipass APIを利用できるのはShopify Plusのみなので)
The Multipass login feature is available to Shopify Plus merchants only.
https://shopify.dev/docs/admin-api/rest/reference/plus/multipass
アプリケーション側の実装については、Dockerで簡単なRailsアプリを準備します。
下準備
まず、Shopify側でMultipass APIを有効化しなければなりません。
Shopify管理画面の左下から「設定」→「チェックアウト」と進み、「顧客アカウント」を任意もしくは必要とした上で「マルチパスを有効にする」をクリックします。
するとシークレットキーが表示されるはずなので、メモに控えておいてください。(後ほどアプリケーション側の実装を行う際に使用します。)
実装
次にアプリケーション側の実装に移ります。
ディレクトリを作成
$ mkdir shopify-multipass-api-on-rails
$ cd shopify-multipass-api-on-rails
各種ファイルを作成
$ touch Dockerfile
$ touch docker-compose.yml
$ touch Gemfile
$ touch Gemfile.lock
FROM ruby:2.6.6
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get install nodejs
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn
ENV APP_PATH /myapp
RUN mkdir $APP_PATH
WORKDIR $APP_PATH
ADD Gemfile $APP_PATH/Gemfile
ADD Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install
ADD . $APP_PATH
version: "3"
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
command: --default-authentication-plugin=mysql_native_password
volumes:
- mysql-data:/var/lib/mysql
ports:
- 3306:3306
web:
build:
context: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/myapp
- ./vendor/bundle:/myapp/vendor/bundle
environment:
TZ: Asia/Tokyo
RAILS_ENV: development
ports:
- 3000:3000
depends_on:
- db
volumes:
mysql-data:
source "https://rubygems.org"
gem "rails", "~>6"
# 空欄でOK
Railsプロジェクトを作成
$ docker-compose run web rails new . --force --no-deps --database=mysql --skip-test --webpacker
Gemfileが更新されたので再度ビルド。
$ docker-compose build
データベースを作成
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: password # デフォルトだと空欄になっているはずなので変更
host: db # デフォルトだとlocalhostになっているはずなので変更
「./config/database.yml」を変更し、データベースを作成。
$ docker-compose run web rails db:create
動作確認
コンテナを起動。
$ docker-compose up -d
http://localhost3000 にアクセスしていつもの画面が表示されればOKです。
認証機能を作成
Deviseなどを使って新規登録/ログインを行うための認証機能を作成していきます。
gem 'devise'
Gemfileを更新したので再度ビルド。
$ docker-compose build
Deviseをインストール
$ docker-compose run web rails g devise:install
...
create config/initializers/devise.rb
create config/locales/devise.en.yml
config以下のファイルが追加されたので、コンテナを再起動します。
$ docker-compose down
$ docker-compose up -d
modelを作成
$ docker-compose run web rails g devise user
...
Running via Spring preloader in process 18
invoke active_record
create db/migrate/20210410093331_devise_create_users.rb
create app/models/user.rb
insert app/models/user.rb
route devise_for :users
$ docker-compose run web rails db:migrate
各種controllerを作成
$ docker-compose run web rails g devise:controllers users
...
Running via Spring preloader in process 18
create app/controllers/users/confirmations_controller.rb
create app/controllers/users/passwords_controller.rb
create app/controllers/users/registrations_controller.rb
create app/controllers/users/sessions_controller.rb
create app/controllers/users/unlocks_controller.rb
create app/controllers/users/omniauth_callbacks_controller.rb
動作確認
http://localhost:3000/users/sign_up にアクセスしして「Sigin up」ページが表示されれば成功です。
home_controllerを作成
$ docker-compose run web rails g controller home index
...
Running via Spring preloader in process 18
create app/controllers/home_controller.rb
route get 'home/index'
invoke erb
create app/views/home
create app/views/home/index.html.erb
invoke helper
create app/helpers/home_helper.rb
invoke assets
invoke scss
create app/assets/stylesheets/home.scss
Rails.application.routes.draw do
root "home#index"
devise_for :users
end
<h1>Home</h1>
<% if user_signed_in? %>
<p><%= link_to "Sign out", destroy_user_session_path, method: :delete %></p>
<p><%= current_user.email %></p>
<% else %>
<p><%= link_to "Log in", new_user_session_path %> / <%= link_to "Sign up", new_user_registration_path %></p>
<% end %>
http://localhost:3000/ にアクセスして「Home」ページが表示されていればOKです。
試しに適当なメールアドレス・パスワードでユーザーを作成してみましょう。
ユーザー作成に成功すると「Home」ページに遷移するはず。メールアドレスが表示されている事からしっかりログイン状態になっているのも確認できますね。
Shopify Multipass APIを導入
最低限の認証機能が準備できたので、いよいよShopify Multipass APIを導入していきます。
ライブラリを作成
$ touch lib/shopify_multipass.rb
require "openssl"
require "time"
require "json"
require "base64"
class ShopifyMultipass
attr_accessor :encryptionKey, :signingKey
# 暗号化キーと署名キーを生成する
def initialize(multipass_secret = nil)
return if multipass_secret.blank?
block_size = 16
hash = OpenSSL::Digest.new("sha256").digest(multipass_secret)
self.encryptionKey = hash[0, block_size]
self.signingKey = hash[block_size, 32]
end
# 顧客情報を暗号化してトークンを生成する
def generate_token(customer_data_hash)
return if !customer_data_hash
customer_data_hash["created_at"] = Time.now.iso8601
cipherText = self.encrypt(customer_data_hash.to_json)
Base64.urlsafe_encode64(cipherText + self.sign(cipherText))
end
def encrypt(plaintext)
cipher = OpenSSL::Cipher.new("aes-128-cbc")
cipher.encrypt
cipher.key = self.encryptionKey
cipher.iv = iv = cipher.random_iv
iv + cipher.update(plaintext) + cipher.final
end
def sign(data)
OpenSSL::HMAC.digest("sha256", self.signingKey, data)
end
# Shopify側で認証を行うための顧客情報とトークンが入ったURLを生成する
def generate_url(customer_data_hash, domain)
return if !domain
return "https://" + domain + "/account/login/multipass/" + self.generate_token(customer_data_hash)
end
end
lib/以下を読み込むために、「./config/application.rb」に次の1行を追加します。
config.autoload_paths += %W(#{config.root}/lib)
config以下のファイルが追加されたので、コンテナを再起動します。
$ docker-compose down
$ docker-compose up -d
shopify_multipass_controllerを作成
$ docker-compose run web rails g controller shopify_multipass confirm login
....
Running via Spring preloader in process 18
create app/controllers/shopify_multipass_controller.rb
route get 'shopify_multipass/confirm'
get 'shopify_multipass/login'
invoke erb
create app/views/shopify_multipass
create app/views/shopify_multipass/confirm.html.erb
create app/views/shopify_multipass/login.html.erb
invoke helper
create app/helpers/shopify_multipass_helper.rb
invoke assets
invoke scss
create app/assets/stylesheets/shopify_multipass.scss
class ShopifyMultipassController < ApplicationController
def confirm
end
def login
# Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意)
customer_data = {
email: current_user.email,
identifier: current_user.id
}
shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー")
# 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」
url = shopify_multipass.generate_url(customer_data, "ストアのドメイン")
redirect_to url
end
end
customer_dataに含める事ができる値
- email: メールアドレス
- first_name: 名
- last_name: 姓
- tag_string: タグ
- identifer: UID
- return_to: 認証後の遷移先(何も指定しない場合は「ストアのドメイン/account」ページに飛ぶ)
- remort_ip: IPアドレス
- addresses: 届け先住所
※メールアドレスのみ必須項目でそれ以外は任意。(今回の例ではユーザーIDをidentiferとして渡しています。)
より詳細な情報は公式ドキュメントを参照。
https://shopify.dev/docs/admin-api/rest/reference/plus/multipass
<h1>ShopifyMultipass</h1>
<p><%= link_to "Multipass login", login_multipass_redirect_path %></p>
authenticate :user do
get "login/multipass", to: "shopify_multipass#confirm"
get "login/multipass/redirect", to: "shopify_multipass#login"
end
「authenticate :user do ~ end」で囲み、ログイン済みのユーザーだけがアクセスできるようにしておきます。
動作確認
http://localhost:3000//login/multipass にアクセスし、「Multipass login」をクリックしてみましょう。
上手くいくとマイページ(ストアのドメイン/account)に遷移するはずです。
Shopify管理画面からもMultipass API経由でユーザーがログインされている事が確認できました。
Tips
これで最小限の実装は完了しましたが、実運用を想定した場合、個人的に気をつけた方が良いと思う点がいくつかあるので記述しておきます。
メールアドレスの整合性について
何よりもまず気になるのがShopify ⇄ SSO基盤間におけるメールアドレスの整合性です。
というのも、両者は別にデータベースを共有しているわけではないため、たとえばSSO基盤側のメールアドレスを変更した際、同時にShopify側のメールアドレスも変更される仕組みを作っておかないと整合性が取れなくなり、全く別のユーザーとしてログインする事になってしまいます。(Shopifyではメールアドレスをユニーク識別子としているため。)
主な解決方法としては、Shopifyが提供しているCustomer APIを使うのが良さげです。
https://shopify.dev/docs/admin-api/rest/reference/customers/customer
実際、自分が携わっているサービスでは、メールアドレス変更時に上記APIを叩いてShopify側が保持しているメールアドレスも変更する事で整合性を保つようにしています。
ログイン後の遷移先について
Multipass APIを利用したログイン後の遷移先を「return_to」で指定できるというのは先述した通りです。
何も指定していないデフォルトの状態だとマイページに飛ぶようになっていますが、良くあるような「ショッピングカート」→「ログイン」→「ショッピングカート(に戻る)」といったフローを実現したい場合はクエリパラメータなどで遷移先のURLを上手く拾ってあげる必要があります。
たとえば、
http://localhost:3000/login/multipass?redirect_uri=ショッピングカートのURL
といった感じで、Shopify → SSO基盤への遷移時にクエリパラメータとしてショッピングカートのURLを渡しておけば、あとは
class ShopifyMultipassController < ApplicationController
def confirm
end
def login
# Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意)
customer_data = {
email: user.email,
identifier: user.id,
}
# クエリパラメータ「redirect_uri」が含まれていた場合は拾う。
uri = URI(request.referer) if request.referer
if uri && uri.query.present?
q_array = URI::decode_www_form(uri.query)
q_hash = Hash[q_array]
customer_data["return_to"] = q_hash["redirect_uri"] if q_hash["redirect_uri"].present?
end
shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー")
# 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」
url = shopify_multipass.generate_url(customer_data, "ストアのドメイン")
redirect_to url
end
end
「request.referer」を使う事でその値を「return_to」内に含める事ができます。
あとがき
以上、Shopify Multipass APIを使ったSSOを試してみました。今回紹介したコードはあくまでサンプルなので、実運用を想定した場合は何かと不備があるかもしれませんが、その辺はご了承ください。
Tipsでも取り上げているように、色々と工夫しなければならない点はあるので、ご自身のプロジェクトの仕様などを踏まえた上で試行錯誤していただければと思います。
今回作成したアプリのソースコード: https://github.com/kazama1209/shopify-multipass-api-on-rails