Share
Embed
ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス
Kaigi on Rails 2025 の発表資料です。
https://kaigionrails.org/2025/talks/kokuyouwind/#day2
kokuyouwind
September 26, 2025
Transcript
58 slides
-
$ whoami 黒曜 / @kokuyouwind Leaner Technologies Inc. 所属 Rails
エンジニア・SRE 今はLeaner の認証基盤サービスを 作ってます 2 -
認証基盤、どう作る? セッション管理 アクセストークン セキュリティ リフレッシュトークン JWT OIDC SSO ローカルストレージ PKCE
nonce state IdP SP Auth0 Cognito ActiveDirectory SAML SCIM 8 -
認証・認可のイメージ( 郵便受取) 1. 私は黒曜です 荷物の受取をお願いします 2. 確かに正当な本⼈証明書でした あなたは黒曜さんです( 認証) 3.
この荷物は黒曜さん宛なので 黒曜さんに渡してOK( 認可) 12 -
認証・認可のイメージ(Web) 1. 私は黒曜です パスワードはxxx です 2. 確かに正しいパスワードでした あなたは黒曜さんです( 認証) 3.
毎回本⼈確認を⾏うと⼤変なので、 クッキーにあなたの情報をメモしました クッキーの中身はサーバーからしか⾒えないので、 以降はこのクッキーを送ってください( セッションの記録) 13 -
認証・認可のイメージ(Web) 4. ファイルにアクセスしたいです クッキーはこれです 5. クッキーの中には「黒曜さん」が 記録されているので、 この⼈は黒曜さんです( セッションの復元) 6.
リクエストされたファイルの持ち主は黒曜さんなので、 黒曜さんにこのファイルを送ってもOK( 認可) 14 -
Rails の認証 よく使われるのは以下のgem Rails 8 では認証ジェネレータも導⼊された bin/rails generate authentication 16:30
からのwillnet さんのセッションで詳しく聞けそう Devise Sorcery Rodauth 17 -
Devise # lib/devise/controllers/sign_in_out.rb def sign_in(resource_or_scope, *args) # ... warden.session_serializer.store(resource, scope)
# ... end # lib/warden/session_serializer.rb def store(user, scope) return unless user method_name = "#{scope}_serialize" specialized = respond_to?(method_name) session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user) end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 session['warden.user.user.key'] = [user.id, user.salt] セッションにユーザーID( とsalt) を保存 18 -
Rails のセッション管理 session[] と session[]= で読み書きできる config.session_store で保存⽅法を決定 デフォルトは CookieStore
暗号化してCookie に丸ごと保存 サーバー側にデータは持たない ミドルウェア系Store(MemCacheStore, Redis::Store など) Cookie にセッションID のみを保存 サーバー側ミドルウェアでセッションID をキーにデータを保存 20 -
認証・認可・セッション管理 まとめ 認証は「本⼈確認」、認可は「権限確認」 認可は都度判断なので認証とは切り離せる 認証結果はセッションに保存する必要がある セッションの保存⽅法は⼤まかに2 種類 CookieStore は暗号化して全部Cookie に⼊れる
ミドルウェア系Store はサーバー側にデータを持って 取り出し⽤の鍵だけCookie に⼊れる 23 -
そもそも認証基盤は必要? ずっと1 プロダクトしか作らないならまず必要ない 分離するコスト・保守するコストがかかる 責務の分割だけなら Rails Engine 切り出しや gem を適切に使えば⼗分そう
ユーザ層が被る複数プロダクトを提供するならほぼ必須 マルチプロダクト戦略・コンパウンド戦略 ユーザ視点・開発視点でメリットが多い 25 -
案A. 認証結果だけ渡してセッション個別管理 認証機能 ログイン ログイン結果連携 機能利⽤ プロダクトA 機能 なんらかの⼿段で認証結果を渡す セッションは各プロダクトで個別に管理
( それぞれのプロダクトのuser.id を保存) (OIDC はセッション管理に責務を持たないのでこれ) 28 -
案B. 前段でセッションを集約管理 認証機能 1. ログイン 3. 機能利⽤ プロダクトA 機能 (ALB
でのCognito 認証連携などはこれ) 2. 認証結果をセッションに記憶 29 4. 認証結果を取り出してプロダクトA に⼀緒に送 信 -
案C. セッションを共有 ( 今回の本題) 認証機能 1. ログイン 4. 機能利⽤ (
鍵を渡す) プロダクトA 機能 2. ユーザーID を記録 3. 鍵を渡す 5. ユーザーID を取得 30 (JWT トークンは鍵⾃体に情報を埋め込むが、原理はこれに近い) -
どれがいいの? 認証情報をOIDC で受け渡す形にすると技術標準に乗れる … が、事前にOAuthApplication 払い出しが必要だったり state, nonce, PKCE などセキュリティ担保のための仕様が多く複雑
OIDC はサードパーティーにID を提供する⽬的の仕様 ⾃社サービスだけで使うのにほんとにそこまで必要? 前段で受けるのはインフラ構成の制約が⼤きい セッションをうまく共有できれば良いのでは? という⽅針でこの後の話をしていきます 31 -
Leaner の認証基盤サービス構成 認証基盤 1. ログイン 4. 機能利⽤ (Cookie も送信) 2.
ユーザーsub を記録 (via. redis-session-store) 3. Cookie にsession_id を保存 5. ユーザーsub を取得 33 -
ログイン ~ ユーザーsub を記録 認証基盤 1. ログイン 4. 機能利⽤ (Cookie
も送信) 2. ユーザーsub を記録 (via. redis-session-store) 3. Cookie にsession_id を保存 5. ユーザーsub を取得 34 -
ログイン ~ ユーザーsub を記録 認証: + セキュリティ強そうなのと拡張性⾼そうなので選定 パスワードログイン: Rodauth 標準
SAML ログイン: で独⾃Feature を実装 セッション管理: Sidekiq とか使うかなと思ってRedis を選定 Rodauth Rodauth Rails ruby-saml redis-session-store 35 -
ログイン ~ ユーザーsub を記録 # app/misc/rodauth_main.rb class RodauthMain < Rodauth::Rails::Auth
configure do after_login do # save user session[:shared_sub] = user.sub end end end 1 2 3 4 5 6 7 8 9 # leaner:session:2::b0db908bd1428e4638... { "shared_sub": "ZiJkFfmqOzSuTtvMwHflJDxWD7h1", "leaner_id_user_id": 6, "leaner_id_active_session_id": "q_KRmXTSw3...", "leaner_id_authenticated_by": [ "password" ] } 1 2 3 4 5 6 7 8 9 36 -
Cookie の保存 ~ 送信 認証基盤 1. ログイン 4. 機能利⽤ (Cookie
も送信) 2. ユーザーsub を記録 (via. redis-session-store) 3. Cookie にsession_id を保存 5. ユーザーsub を取得 37 -
Cookie の保存 ~ 送信 認証基盤 auth.leaner.app mitsumori.leaner.app 認証基盤とプロダクトはURL が異なる (
後出しの制約) Cookie は基本的に同⼀ドメインにしか送らないため、 認証基盤でセッションID をCookie に格納しても プロダクト側に送られない! 38 -
Cookie の保存 ~ 送信 # config/application.rb Rails.application.config.session_store :redis_session_store, key: 'LEANER_SESSION_ID',
domain: '.leaner.app', secure: true, same_site: :lax, serializer: :json, redis: { # ... } 1 2 3 4 5 6 7 8 9 10 Cookie に domain を指定すると、後⽅⼀致で送信するか決まる こうすると auth.leaner.app と mitsumori.leaner.app の 両⽅に同じCookie を送信する 39 -
ユーザーsub を取得 認証基盤 1. ログイン 4. 機能利⽤ (Cookie も送信) 2.
ユーザーsub を記録 (via. redis-session-store) 3. Cookie にsession_id を保存 5. ユーザーsub を取得 40 -
ユーザーsub を取得 # leaner:session:2::b0db908bd1428e4638... { "shared_sub": "ZiJkFfmqOzSuTtvMwHflJDxWD7h1", "leaner_id_user_id": 6, "leaner_id_active_session_id":
"q_KRmXTSw3...", "leaner_id_authenticated_by": [ "password" ] } 1 2 3 4 5 6 7 8 9 # app/controller/application_controller.rb class ApplicationController < ActionController::API def current_user User.find_by(sub: session[:shared_sub]) end end 1 2 3 4 5 6 41 -
Leaner の認証基盤サービス構成( 再掲) 認証基盤 1. ログイン 4. 機能利⽤ (Cookie も送信)
2. ユーザーsub を記録 (via. redis-session-store) 3. Cookie にsession_id を保存 5. ユーザーsub を取得 42 -
プロダクト側で使う独⾃gem LeanerAuthenticatable gem を使って プロダクト側処理を共通化 # app/controller/application_controller.rb class ApplicationController <
ActionController::API # 認証基盤からユーザーを取得する include LeanerAuthenticatable.create_module( organization_class: Organization, user_assoc_method: 'users', product_code: 'mitsumori', session_key_prefix: 'mitsumori' ) # 以下メソッドが利⽤できるようになる # def current_user: () -> User end 1 2 3 4 5 6 7 8 9 10 11 12 13 45 -
組織からのユーザー取得 # app/controller/application_controller.rb class ApplicationController < ActionController::API # 認証基盤からユーザーを取得する include
LeanerAuthenticatable.create_module( organization_class: Organization, user_assoc_method: 'users', # Organization # .find_by(sub: session[:shared_organization_sub]) # .users.find_by(sub: session[:shared_sub]) 1 2 3 4 5 6 7 8 9 # leaner:session:2::b0db908bd1428e4638... { "shared_organization_sub": "M0qMtiOCrv5...", "shared_sub": "ZiJkFfmqOzSuTtvMwHflJDxWD7h1", "shared_product_licenses": [ "mitsumori" ], ... } 1 2 3 4 5 6 7 8 9 46 -
プロダクト利⽤権の判定 # app/controller/application_controller.rb class ApplicationController < ActionController::API # 認証基盤からユーザーを取得する include
LeanerAuthenticatable.create_module( # ... product_code: 'mitsumori', # session[:shared_product_licenses] # .member?('mitsumori') 1 2 3 4 5 6 7 8 # leaner:session:2::b0db908bd1428e4638... { "shared_organization_sub": "M0qMtiOCrv5...", "shared_sub": "ZiJkFfmqOzSuTtvMwHflJDxWD7h1", "shared_product_licenses": [ "mitsumori" ], ... } 1 2 3 4 5 6 7 8 9 47 -
session の名前空間分割 # app/controller/application_controller.rb class ApplicationController < ActionController::API # 認証基盤からユーザーを取得する
include LeanerAuthenticatable.create_module( # ... session_key_prefix: 'mitsumori' # class SessionStoreWrapper # def [](key) # parent[shared_key?(key) # ? key : :"#{prefix}_#{key}"] # end 1 2 3 4 5 6 7 8 9 10 11 # leaner:session:2::b0db908bd1428e4638... { # session[:login_method] = "leaner_auth" "mitsumori_login_method": "leaner_auth" # shared_ から始まるキーはそのまま読み書きできる "shared_sub": "ZiJkFfmqOzSuTtvMwHflJDxWD7h1", ... 1 2 3 4 5 6 7 48 -
# docker-compose.yml services: auth_server: image: ghcr.io/.../leaner-auth/leaner-auth-api:latest # <= 認証基盤 redis:
image: redis:alpine # <= session store ⽤のredis proxy: image: ghcr.io/.../leaner-auth/minica-proxy:latest # <= ??? 1 2 3 4 5 6 7 8 ローカル開発⽤ 認証基盤コンテナ プロダクト側のローカル開発時に使えるよう、 認証基盤をコンテナ化してGitHub Packages で配信 50 -
ローカル開発⽤ プロキシ ローカル開発でもCookie でセッション共有できるよう minica を使って証明書を発⾏、 nginx でプロキシ 認証基盤 https://auth.leaner.localhost
https://mitsumori.leaner.localhost w/Cookie SetCookie domain: .leaner.localhost 51 -
Q. CookieStore じゃできないの? A. secret_key 依存があるので厳しい CookieStore ではセッション値を暗号化して設定するが、 このときの暗号化鍵が Rails
の secret_key_base に依存している。 すでに稼働しているプロダクトの secret_key_base を共通化するのは厳しい。 RedisStore ではセッションID からRedis key を計算する際 ハッシュ関数しか使わないので、どのプロダクトで計算しても同じになる。 多分他のミドルウェア系SessionStore でも同じ感じのはず。 54 -
Q. Rodauth どう? A. 慣れると結構良いがおすすめはしない めちゃくちゃ柔軟性が⾼くカスタム処理が書きやすいが、 独⾃DSL でかなり慣れが必要なうえ、 ドキュメントはRDoc メインで使い⽅例みたいなのはほとんどない。
困ったら元コードを掘るパワーが必要。 あとAI との相性が死ぬほど悪い。 正直素直に広く使われているdevise とかを使うほうが AI のサポートを受けやすくて良いと思う。 55 -
Q. 既存のIDaaS 使わないの? A. 検討したけどコスパや柔軟性でやめた 正直むずかしいのは認証というよりセッション同期 SAML 使うと⼀気に⾼くなり、⻑期的には独⾃でやるほうが良いと判断 実は Google
Identity Platform を認証基盤化するつもりで⼀部採⽤してたが パスワードポリシーが弄れないなど⾊々しんどくて諦めた パスワードハッシュexport したらBcrypt じゃなく独⾃scrypt で 計算するのにC build が必要と⾔われてインポートを泣く泣く諦め ログイン時の都度migration に倒している 56 -
まとめ マルチプロダクト戦略だと認証基盤サービスが欲しくなる 実現⽅法はいろいろある 認証結果の連携はOIDC が技術標準だが、 サードパーティーを考慮していて仕様が複雑 ⾃社サービス内だけならセッション共有がシンプルでは Cookie を親レベルにすればRails の設定レベルで実現できる
共通ドメインが必要など制約も多いので、ハマる局⾯は限られる これが良いやり⽅か⾃分もわからないので、⾊々話しましょう! 58