BtoB SaaS における Rails API と セッションについて考える
Leaner Technologies の @corocn です。
この記事は "Digital Identity技術勉強会 #iddance Advent Calendar 2022" の 19日目の記事です。
今年はプロダクトマネージャーとしての仕事で頭の中がいっぱいだったので、Digital Identity周りのキャッチアップができませんでした。来年は Passkeys 等キャッチアップしていきたいですね。
ということで、直近お仕事で使っている Ruby on Rails に特化した話を書きます。
本記事の概要
- Rails API mode と SPAの構成でプロダクトを立ち上げる場合でのセッション周りでの学び
- 次に新規で立ち上げるならどんなセッションの仕組みにするか
話さないこと
- なぜ Rails API と SPA の構成なのか
- 他のフレームワークの話(話すことができないが正しい)
- 3rd Party向けの認証認可
考えていること
BtoB or BtoC、スモールビジネス or Enterprise、マルチプロダクト、マネタイズ方針、チームメンバーのスキルセット、ありたい組織の姿で状況は変わってくるし、状況にあった技術選定が必要になる。
ただ、次にBtoB SaaSを新規で立ち上げるなら、どんな順序で考えていくといいか、自分の中で整理されてきたので、年の瀬に書き残しておく。
置かれている状況
- 自分
- BtoB SaaS を開発しているプロダクトマネージャー兼エンジニア
- お客様への価値提供を最優先としたいが、セキュリティ面も優先度が高く心配
- プロダクト
- PMF するかしないかぐらいの時期で、ユーザーもまだ少ない
- Rails API と SPA で構成されている
- 特定領域の業務システムであり、ログインを前提として利用する
- ブラウザからのアクセスのみ考慮すればよい
- チーム
- エンジニア4人の小さいチーム
- インフラ専任がいないため、大掛かりなアーキテクチャにしたくない
- Rails のエコシステムにうまく乗って設計したい
本記事は、上記視点で書かれている
Rails API mode について
次の設定を有効にすると API mode になり、JSONを返すだけのシンプルなAPIが作れるようになる。
config.api_only = true
API mode では、デフォルトではセッション機能を持っていない。追加でミドルウェアを有効にすることによって、Cookieベースのセッション機能を有効にすることができる。
つまり API mode は、デフォルトではステートレスな API を前提としており、セッション管理は別で考えてくださいね、というスタンスを感じている。
まずは IDaaS の利用を検討する
Rails API がセッションの面倒を見てくれないとしたら、外部サービスで担保することになる。
-
Pros
- さくっと立ち上げるのに便利
- 認可基盤の管理が不要で、価値提供の時間にできる
-
Cons
- IDaaSの利用料金がサービスのプライシングに影響する
- BtoBで必須のSAMLは、ほぼエンタープライズプランが多く、高額
- エンプラ向けの特殊要件への対応で結局カスタマイズが必要なる場合がある
- ユーザー基盤、認証基盤とセットになる
IDaaS を利用する場合は、IDaaS とセッションが貼られ、独自API向けのトークンを払い出すケースが多い。Auth0 や Firebase がそうだ。厳密にいえば Firebase は払い出されるID Tokenをセッションのように使えてしまうというだけだが、デフォルト状態の Rails API とは相性が良い。
次に考えるのは Cookieベース
伝統的な手法であるし、MPAであればかなりの部分を Rails が面倒見てくれる。SPAになると参考にできそうな資料は少なくなる。
Rails の場合は次の設定で有効にすることができる。
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
- Pros
- Railsの標準機能でセッション管理ができる
- フロントエンドでトークンのローテーションを考えなくてよい
- セッションのコントロールがしやすい
- Cons
- SPA側でのCSRFトークンの扱いが自前実装になるのが怖い
- APIを増やした時にセキュリティホールを作る可能性がある
- 本番とローカルで挙動が異なる場合がある
- プライバシー起点でCookie自体の仕様が変更されがち(主に 3rd party cookieが多いが)
- APIの単体での動作確認がしづらいケースがある
- クライアントにモバイルアプリが選択肢にあがる場合は選択肢から外れる
Railsの標準機能を使えば一定のセキュリティが担保できる。IDaaSが料金的に見合わず、1 SPA + 1 API であるシンプルなWebアプリケーションである場合はこの方法を選択すると思う。
CSRF対策
CSRF対策が若干面倒かつ怖いポイントでもあると思っている。Railsの標準機能を活用しようとするとステートフルなCSRF対策になる。ステートレスなCSRF対策もあるが、ネット上に転がっている実装例も少なく、自前実装してセキュリティホールになる可能性も高い。
Cookie x CORS x CSRF あたりを一貫して考え出した時に混乱してしまうことが多いので、落ち着いて整理することが必要。
本番と開発環境での差異
本番と開発環境での挙動の差異が、立ち上げ時期では地味につらいことがある。特に障害発生時。
Cookieはポート番号を無視するので、localhost で開発していると、完全に同一ドメインとして扱われる。domain を指定しなくても Cookie が送信される。このため、ローカルでも localhost を使わない開発をしている人もちらほら見かけるし、それが正しいのかもしれない。
本番の場合はドメインが異なるので、開発環境では動くが、本番環境で動かないというつらい状況を引き起こしやすい。また、CORS の Same Origin Policy と混同しがち。
最近だと API 開発をするための便利ツールが諸々でてきているが、ツール側がトークンベースの認可の仕組みしか対応していないケースもある。ただし、ローカル環境のみで有効なアクセストークンを作ったり、ミドルウェアでラップするとか、そこまで重くない工夫で対応できそうではある。
また、CookieStore だとセッションに内容が暗号化された状態でCookie内に保存されてしまうので、できればセッションIDをランダムな文字列にして、Redisなどのmemorystoreに保存したほうがよい。公式ではないが gem として提供されている。
インフラ側で見る項目も増えるが、Sidekiq などを使い始めると結局 Redis が必要になるケースも多い。
危険そうなパターン
求人マッチングサイトのような、利用者が2パターンに分かれて画面も分かれるケースはちょっと気をつけたほうが良い。
SPAが2種類以上あるようなケースではCookie自体を諦めるか、API自体のドメイン自体を分けたほうがよい。
たまに見るのが、Rails の API は1ドメインにしていて、Railsのコントローラーレベルで処理を分離しているケース。
徐々に機能拡張していくことで、最初は 1SPA 1API のシンプルなWebアプリケーションだったので、パスベースで機能を生やしてしまう。例としては以下。
- https://api.example.com/app -> ユーザーからのアクセス
- https://api.example.com/business -> 企業側からのアクセス
- https://api.example.com/admin -> 運営側、サービス提供側からのアクセス
Railsは標準でマルチセッションに対応していないため、上記のようなAPIで、あるユーザーが2つの画面への同時ログインを認めるようなサービス設計をしてしまうと、セッションを共有することになる。ドメインが一緒なので無邪気にやるとこうなる。
session[:customer_user_id] = ユーザーのID
session[:business_user_id] = 企業側のユーザーID
上記のようなケースで、適切にアプリケーションレイヤーで防がないと、ユーザー <> 企業間でCSRFの脆弱性が生まれやすくなる。
APIサーバーが1つでも、BFF/リバースプロキシをかますなりして、画面ごとにAPIのドメインを分けて、適切にCookieを制御できるようにしたほうがよさそう。ただし、間に1層かませることになるので、開発時に考慮するものも増えてしまう。
ここまでの結論としては、APIが2種類のドメインから利用されるようになった時点で、状況を見て、トークンベースの認証を考えはじめればいい。
運営画面(サービス提供側)が使うぐらいであれば、メインのユーザー画面のみ Cookie で制御し、運営画面は Auth0 にセッションを任せちゃうとかでもよさそうである。
最後に考える トークンベース
トークンベースと書いてあるが、基本的には OAuth 2.0 の Authorization Code Flow with PKCE の仕組みに乗っかる。
- Pros
- SPA や API が増えたときに対応しやすい
- 本番とローカルでの差異を気にする必要がなくなる
- CSRFの心配がなくなる
- ツールやコマンドラインでの動作確認がしやすい
- Cons
- フロントエンドでのローテーションが面倒
- アクセストークンを即時無効化する必要がある場合に対策が必要
- Rails 向けのシンプルな gem が少ないので自前実装になりそう
CookieStore は意外と便利
モジュラモノリス的な考えでセッショントークンを払い出す場合に使いやすい。認証は別サービスで SSO して、セッションは独自で担保するようなケース。
SSO を考える際に、OAuth 2.0 や OpenID Connect 文脈の state や nonce など、一時的なセッションを必要とする場合がある。そのときに便利に利用できる。
DeviseTokenAuth
Rails には Devise という認証 gem が存在する。また、Devise と組み合わせてSPA向けのトークンを発行する DeviseTokenAuth gem というものがある。
DeviseTokenAuthは 独自仕様であり、認証とセッションが密結合され、特殊なロジックを差し込もうとした瞬間に辛く、そこを起点としてセキュリティホールを作る可能性もあるので推奨したくない。Devise を剥がそうとすると、まず DeviseTokenAuth を剥がさなきゃいけない、といった形になる。
Doorkeeper という OAuth 対応のgemも存在するが、基本的には 3rd party 向けの認可の仕組みを提供するものであるため、やりたいことに対して少し重そう。良い gem があればぜひ教えてほしい。
Rails の 認証関連の gem は、勝手にAPIが生えてしまうものも多く、過去にも勝手に生成された画面に気づかなくてインシデントを起こした事例を見たことがあるので、ルーティングと密結合になるものは基本的に推奨したくない。
OAuth 2.0 for Browser-Based Apps
draft ではあるが、OAuth 2.0 for Browser-Based Apps という 1st party 向けのアクセストークンの RFC も存在する。これを自前実装という手もある。draft ではあるが、OAuth 2.0 Authorization Code Flow with PKCE がベースになっているという理解。
トークンの保管場所
度々議論になるトークンの管理場所に関するよくある懸念はこちらで解消された。
アクセストークンの無効化
無効化の要件次第では、アクセストークン自体もステートフルにしてしまっても良いかもと考えている。ステートレスというのメリットは薄れるが、Cookieを使って複雑な部分を自前実装しなきゃいけないケースを回避できるというメリットもありそう。
また、小規模なアプリケーションであれば、アクセストークン自体を無効化にしなくても、アプリケーションレイヤーでブロックする仕組みを用意するなど、色々と対策はできそう。
まとめ
- Rails API の 特定のユースケース下でのセッションについて考えた
- 小さいチームにフィットする Rails のエコシステムにうまく乗って設計したい
- シンプルなアプリケーションであれば、まずは Cookie をベースに考えると良いと思う
認証やセッション周りはセキュリティと隣合わせで、様々な要素技術を熟知していけなければいけない。もっと RFC読んで実装してみたり、より深い知識を身につけることで、様々なプロダクトやチームの状況を見て最適な提案ができるようになれそうだと思った。
仕事
Leaner Technologies では一緒に働いてくれるエンジニアを募集しています。
法人が物を買う時の見積・購買のプロセスは闇が深すぎて解決しがいがある領域です。
ツイッター等でもいいので気軽に声をかけてください。
Discussion