GANMA! の GraphQL に persisted documents を実装しました!
こんにちは。 GANMA! では通信に GraphQL を利用しており、今回 persisted documents を実装しました。その経緯についてお伝えします。
背景
GANMA! は iOS, Android アプリと web で展開しており、それぞれのクライアントで求められるデータが微妙に異なります。
またアプリ開発とサーバーサイドで開発者が分かれており、人とシステムの境目であるAPIが柔軟性を持つことで、お互いの仕事をブロッキングしづらくするという意味でも GraphQL を採用しています。
クライアントから柔軟なクエリができる点が GraphQL の良い点ですが、悪意のある攻撃者にとって都合の良い側面もあります。
例えば一度のリクエストで多数のデータを要求することでサーバー負荷を上昇させ、サービス停止に追い込む攻撃などが考えられます。
この攻撃を成立させないために一度にリクエストできるデータの量を制限するプラクティスが存在しており、GANMA!でも同様の仕組みを展開していました。
しかし、クライアントアプリの発展に伴い、多くのデータを一度にリクエストしたい場面がでてきました。しばらくはリクエストできるデータ量の閾値を調整してしのいでいたのですが、データ量を上げることはセキュリティを緩めることにつながるため、次第に他の仕組みを考えるようになりました。
開発者が作成した信頼できるクエリのみを実行し、それ以外のクエリは実行できないようにすることで、セキュリティを担保しつつクライアントのニーズに応えられます。
ふたつの信頼の選択肢
信頼したクエリのみ実行する方法として大きく2つの方向性が考えられました。
- クエリを事前登録する
- クエリに署名する
最初に考えられるのは、開発時にクエリをどこかに登録しておき、プロダクションでは登録したクエリのみ実行するという方法です。
調べてみると、2024年12月時点では各種プラットフォームが独自に実装している段階で Relay, Apollo, Yoga など別々の通信仕様となっていました。
GraphQL-over-HTTP では 標準化の動き がありますが、まだ標準化されていないという状況です。
もう一つの方法は、クエリに署名する方法です。このアイデアは スタディサプリのblog で知りました。
クライアントとサーバーで鍵交換しておき、電子署名を付与して送付することでクエリの信頼性を担保する方法です。
事前登録作業が必要ないという点で優れていますが、鍵がキーなので(?)しっかり管理する必要があります。
我々はクエリを事前登録する方法を選択しました。その際に考えたことは以下のようなことです。
- アプリで Apollo SDK を用いているため、Apollo の persisted documents 仕様に沿えば、サーバー実装のみで実現できそう
- 署名の場合、しっかりした鍵の管理(体制構築・ローテーション)が案外大変そう
- 事前登録を CI に仕込み、サーバーは動的に読み込むようにすれば、登録漏れによる事故は起きないだろう
実装概要
クエリを DynamoDB へ事前登録し、サーバーからは DynamoDB に随時問い合わせてクエリを解決するようにしました。
Apollo の仕様に沿うようにサーバーを実装し、事前登録されたクエリ以外は拒否します。
CIから事前登録コマンド[1]を実行するようにし、Apollo が生成する manifest ファイル (JSON) を DynamoDB へ登録するようにしました。
webのクライアントでは Apollo を使用していなかったため、 graphql-codegen で Apollo の manifest を生成する plugin を実装しました。
一部の開発運用業務で事前登録していないクエリを実行したいケースがあったので、開発者向けの短期トークン発行と、トークンが付与されたリクエストは事前登録されていなくても実行できる仕組みを整えました。
まとめ
信頼できるクエリのみを実行するようにすることで GraphQL の安全性を高めることができます。
事前登録と署名が考えられ、我々は事前登録でハッピーになりました。
GANMA!ではGraphQLをふくむ様々な技術を取り入れながら開発をしており、これからもユーザーに価値を届けられるように邁進していきます。
Links
-
コマンドは Copilot を使って Go で開発しました。単純で短いソースコードだと生成AIは本当によく働きますね。 ↩︎
Discussion