$30 off During Our Annual Pro Sale. View Details »
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス
Search
kokuyouwind
September 26, 2025
Programming
0
0
ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス
Kaigi on Rails 2025 の発表資料です。
https://kaigionrails.org/2025/talks/kokuyouwind/#day2
kokuyouwind
September 26, 2025
Tweet
Share
More Decks by kokuyouwind
See All by kokuyouwind
謎解きサイトを Rails SPA で作って RubyKaigi で配布した話
kokuyouwind
0
17
Do LLMs dream of Type Inference?
kokuyouwind
0
0
Let's use LLMs from Ruby 〜 Refine RBS types using LLM 〜
kokuyouwind
0
7.5k
APMをちゃんと使おうとしたら、いつのまにか独自gemを作っていた話
kokuyouwind
0
870
RBS meets LLMs - Type inference using LLM
kokuyouwind
0
910
オンラインボードゲームを作りたい人生だった
kokuyouwind
0
600
1年間本番運用してわかった、スタートアップこそAWS Copilot CLIを使うべきNつの理由
kokuyouwind
2
11k
なるべく楽したいAWSセキュリティ
kokuyouwind
1
84
Railsパフォーマンス・チューニング入門
kokuyouwind
0
350
Other Decks in Programming
See All in Programming
Navigation 3: 적응형 UI를 위한 앱 탐색
fornewid
1
410
Go コードベースの構成と AI コンテキスト定義
andpad
0
130
tparseでgo testの出力を見やすくする
utgwkk
2
260
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
210
「コードは上から下へ読むのが一番」と思った時に、思い出してほしい話
panda728
PRO
39
26k
Developing static sites with Ruby
okuramasafumi
0
310
認証・認可の基本を学ぼう後編
kouyuume
0
240
Graviton と Nitro と私
maroon1st
0
110
tsgolintはいかにしてtypescript-goの非公開APIを呼び出しているのか
syumai
7
2.3k
AIの誤りが許されない業務システムにおいて“信頼されるAI” を目指す / building-trusted-ai-systems
yuya4
6
3.8k
re:Invent 2025 のイケてるサービスを紹介する
maroon1st
0
140
ローターアクトEクラブ アメリカンナイト:川端 柚菜 氏(Japan O.K. ローターアクトEクラブ 会長):2720 Japan O.K. ロータリーEクラブ2025年12月1日卓話
2720japanoke
0
740
Featured
See All Featured
Fireside Chat
paigeccino
41
3.8k
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
1
16
Building Flexible Design Systems
yeseniaperezcruz
330
39k
Technical Leadership for Architectural Decision Making
baasie
0
180
WCS-LA-2024
lcolladotor
0
380
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
0
67
Game over? The fight for quality and originality in the time of robots
wayneb77
1
61
Digital Ethics as a Driver of Design Innovation
axbom
PRO
0
130
The Invisible Side of Design
smashingmag
302
51k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
How to Ace a Technical Interview
jacobian
281
24k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.7k
Transcript
ドメイン指定Cookie と サービス間共有Redis で作る 認証基盤サービス Leaner Technologies, Inc. 黒曜 (@kokuyouwind)
1
$ whoami 黒曜 / @kokuyouwind Leaner Technologies Inc. 所属 Rails
エンジニア・SRE 今はLeaner の認証基盤サービスを 作ってます 2
認証基盤サービス? 3
シンプルに Rails アプリを作る場合、 認証( ログイン) も1 つの機能として実装 認証機能 プロダクト機能 ログイン
機能利⽤ 4
複数のアプリがあるとそれぞれログインが必要 認証機能 プロダクトA 機能 認証機能 プロダクトB 機能 ログイン 機能利⽤ ログイン
機能利⽤ 5
認証機能を単独のサービスに切り出す 認証機能 プロダクトB 機能 ログイン 機能利⽤ 機能利⽤ 6 プロダクトA 機能
認証基盤サービス 認証機能 プロダクトB 機能 ログイン 機能利⽤ 機能利⽤ プロダクトA 機能 7
認証基盤、どう作る? セッション管理 アクセストークン セキュリティ リフレッシュトークン JWT OIDC SSO ローカルストレージ PKCE
nonce state IdP SP Auth0 Cognito ActiveDirectory SAML SCIM 8
弊社の認証基盤で採⽤した 「ドメイン指定Cookie + 共有Redis 」構成の 紹介をします 9
アジェンダ 認証・認可・セッション管理 認証共通化のアーキテクチャ選択肢 Leaner の認証基盤サービス構成 各種Tips まとめ 10
認証・認可・セッション管理 認証共通化のアーキテクチャ選択肢 Leaner の認証基盤サービス構成 各種Tips まとめ アジェンダ 11
認証・認可のイメージ( 郵便受取) 1. 私は黒曜です 荷物の受取をお願いします 2. 確かに正当な本⼈証明書でした あなたは黒曜さんです( 認証) 3.
この荷物は黒曜さん宛なので 黒曜さんに渡してOK( 認可) 12
認証・認可のイメージ(Web) 1. 私は黒曜です パスワードはxxx です 2. 確かに正しいパスワードでした あなたは黒曜さんです( 認証) 3.
毎回本⼈確認を⾏うと⼤変なので、 クッキーにあなたの情報をメモしました クッキーの中身はサーバーからしか⾒えないので、 以降はこのクッキーを送ってください( セッションの記録) 13
認証・認可のイメージ(Web) 4. ファイルにアクセスしたいです クッキーはこれです 5. クッキーの中には「黒曜さん」が 記録されているので、 この⼈は黒曜さんです( セッションの復元) 6.
リクエストされたファイルの持ち主は黒曜さんなので、 黒曜さんにこのファイルを送ってもOK( 認可) 14
リアルとWeb の違い HTTP はステートレス 認証リクエストと、認可の必要なリクエストは独⽴ 認証の結果をセッションに保存することで、 以降のリクエストの認可制御が⾏える 認証機能を切り出そうと考えると… 認可は切り分けて考えやすそう セッション管理は密接に関連しそう
15
ここからは、 「認証結果をどうセッションに保存するか」に フォーカスして掘り下げます 16
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
基本的には User Model のid を session[]= で保存するだけ 19
Rails のセッション管理 session[] と session[]= で読み書きできる config.session_store で保存⽅法を決定 デフォルトは CookieStore
暗号化してCookie に丸ごと保存 サーバー側にデータは持たない ミドルウェア系Store(MemCacheStore, Redis::Store など) Cookie にセッションID のみを保存 サーバー側ミドルウェアでセッションID をキーにデータを保存 20
CookieStore イメージ 私の情報はこの⾦庫に⼊ってます 鍵を使って中身を取り出しました 黒曜さんのuser.id が書いてあるので この⼈は黒曜さんですね 21
ミドルウェア系のStore イメージ 私の情報は16 番⾦庫に⼊ってます 16 番⾦庫からメモを取り出しました 黒曜さんのuser.id が書いてあるので この⼈は黒曜さんですね 22
認証・認可・セッション管理 まとめ 認証は「本⼈確認」、認可は「権限確認」 認可は都度判断なので認証とは切り離せる 認証結果はセッションに保存する必要がある セッションの保存⽅法は⼤まかに2 種類 CookieStore は暗号化して全部Cookie に⼊れる
ミドルウェア系Store はサーバー側にデータを持って 取り出し⽤の鍵だけCookie に⼊れる 23
認証・認可・セッション管理 認証共通化のアーキテクチャ選択肢 Leaner の認証基盤サービス構成 各種Tips まとめ アジェンダ 24
そもそも認証基盤は必要? ずっと1 プロダクトしか作らないならまず必要ない 分離するコスト・保守するコストがかかる 責務の分割だけなら Rails Engine 切り出しや gem を適切に使えば⼗分そう
ユーザ層が被る複数プロダクトを提供するならほぼ必須 マルチプロダクト戦略・コンパウンド戦略 ユーザ視点・開発視点でメリットが多い 25
認証機能を単独のサービスに切り出す( 再掲) 認証機能 プロダクトB 機能 ログイン 機能利⽤ 機能利⽤ プロダクトA 機能
26
課題 認証機能 ログイン 機能利⽤ プロダクトA 機能 認証した結果をどう渡す? セッションをそれぞれで管理する? なんらかの⽅法で共通化する? 27
案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 の認証基盤サービス構成 各種Tips まとめ アジェンダ 32
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
認証・認可・セッション管理 認証共通化のアーキテクチャ選択肢 Leaner の認証基盤サービス構成 各種Tips まとめ アジェンダ 43
認証基盤の実装詳細・運⽤など 44
プロダクト側で使う独⾃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
gem のリリースとバージョン管理 GitHub Packages で配信し、 プロダクト側のGemfile で依存バージョンを管理 49
# 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
データの同期 認証基盤と各プロダクトでDB が異なるため、 sub をキーとしてデータの同期が必要 ( 現在はプロダクト側にそれぞれユーザー管理がある) 情報変更時の都度同期のほか、⼀括同期タスクも作成 認証基盤 POST
/internal/users/[:sub] 52
想定 Q & A 53
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
認証・認可・セッション管理 認証共通化のアーキテクチャ選択肢 Leaner の認証基盤サービス構成 各種Tips まとめ アジェンダ 57
まとめ マルチプロダクト戦略だと認証基盤サービスが欲しくなる 実現⽅法はいろいろある 認証結果の連携はOIDC が技術標準だが、 サードパーティーを考慮していて仕様が複雑 ⾃社サービス内だけならセッション共有がシンプルでは Cookie を親レベルにすればRails の設定レベルで実現できる
共通ドメインが必要など制約も多いので、ハマる局⾯は限られる これが良いやり⽅か⾃分もわからないので、⾊々話しましょう! 58