Ory Hydra は、アプリケーションに安全な認証と認可を提供する、人気のあるオープンソース OAuth2 および OpenID Connectサーバーです。スケーラブルでパフォーマンスの高い OAuth2 サーバーを構築する際の重要な課題の 1 つは、データベースへのデータの保存と取得を含む永続化レイヤーの管理です。
Ory は、人気のあるサービス プロバイダーから、高負荷時の認証システムのパフォーマンスを最適化するよう打診されました。ピーク時 (600 ログイン/秒以上) の大量の認可付与に対処するのに苦労することがよくありました。別の解決策を探して、彼らは Ory Hydra の評価を開始し、この量の助成金を処理できるかどうかを調査しました。チームに連絡を取った後、Ory Hydra をこれまで以上に高速かつスケーラブルにするために、全体的なパフォーマンスを向上させる方法の調査を開始しました。成功の鍵は、Hydra の永続化レイヤーの一部を再設計してデータベースへの書き込みトラフィックを削減し、一時的な OAuth2 フローに移行することでした。
この作業の中核部分の 1 つは、OAuth2 フローに関与する 3 者間で交換される一時的な OAuth2 フロー状態の大部分をサーバーからクライアントに移動することでした。一時的な状態をデータベースに保持する代わりに、状態はリダイレクト URL 内の AEAD エンコードされた Cookie または AEAD エンコードされたクエリ パラメータとして当事者間で受け渡されるようになりました。 AEAD は、関連データを使用した認証済み暗号化の略です。これは、データが機密であり、秘密 (対称) キーを知らなければ改ざんできないことを意味します。
フローは、最終的な同意が与えられたときに 1 回だけデータベースに保存されます。
この変更にはいくつかの利点があります。まず、データベースに保存する必要があるデータの量が減り、結果的に書き込みトラフィックが減ります。第 2 に、以前は交換中に使用されていたフロー テーブル上の複数のインデックスが不要になります。
最適化したい OAuth2 フローの関連部分は、クライアント (ユーザーの代わりに動作)、Hydra (Ory の OAuth2 認証サーバー)、およびログイン画面と同意画面の間の交換です。クライアントが認可コード付与を通じて認可コードをリクエストすると、ユーザーはまずログイン UI にリダイレクトされて認証され、次に同意 UI にリダイレクトされてユーザーのデータ (電子メール アドレスやプロフィール情報など) へのアクセスが許可されます。
以下は交換のシーケンス図です。各 UI が URL パラメーターの一部としてCHALLENGE
を取得し (ステップ 3 および 12)、このCHALLENGE
パラメーターとして使用して詳細情報を取得することを確認します (ステップ 4 および 13)。最後に、両方の UI は、通常、UI とのユーザーの対話 (ステップ 6 ~ 8 および 15 ~ 17) に基づいて、ユーザー要求を受け入れるか拒否します。この API コントラクトにより、Ory Hydra はヘッドレス状態に保たれ、カスタム UI から切り離されます。
データベースへのアクセスを減らすために、AEAD でエンコードされたフローをLOGIN_CHALLENGE
、 LOGIN_VERIFIER
、 CONSENT_CHALLENGE
、およびCONSENT_VERIFIER
として渡すようになりました。このようにして、OAuth2 フローに関与する関係者に依存して、関連する状態を渡します。
前 | 後 |
---|---|
ログインおよび同意のチャレンジと検証者は、データベースに保存されるランダムな UUID です。 | ログインと同意のチャレンジと検証者は、AEAD でエンコードされたフローです。 |
UI からのリクエストの承認または拒否には、特定のチャレンジのデータベース検索が含まれます。 | UI からのリクエストの承認または拒否には、チャレンジ内のフローを復号化し、検証者の一部として更新されたフローを生成することが含まれます。 |
Ory Hydra はオープンソースであるため、Ory GitHub リポジトリでコードの変更を確認できます。これは関連するコミットです。
ここでは、特定のチャレンジと検証者のフローをエンコードします。
// ToLoginChallenge converts the flow into a login challenge. func (f *Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginChallenge) } // ToLoginVerifier converts the flow into a login verifier. func (f *Flow) ToLoginVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginVerifier) } // ToConsentChallenge converts the flow into a consent challenge. func (f *Flow) ToConsentChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) { return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentChallenge) } // ToConsentVerifier converts the flow into a consent verifier. func (f *Flow) ToConsentVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) { return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentVerifier) }
次に、パーシスター (データベース リポジトリ) で、チャレンジに含まれるフローをデコードします。たとえば、同意チャレンジを処理するコードは次のとおりです。
func (p *Persister) GetFlowByConsentChallenge(ctx context.Context, challenge string) (*flow.Flow, error) { ctx, span := prTracer(ctx).Tracer().Start(ctx, "persistence.sql.GetFlowByConsentChallenge") defer span.End() // challenge contains the flow. f, err := flowctx.Decode[flow.Flow](ctx, prFlowCipher(), challenge, flowctx.AsConsentChallenge) if err != nil { return nil, errorsx.WithStack(x.ErrNotFound) } if f.NID != p.NetworkID(ctx) { return nil, errorsx.WithStack(x.ErrNotFound) } if f.RequestedAt.Add(p.config.ConsentRequestMaxAge(ctx)).Before(time.Now()) { return nil, errorsx.WithStack(fosite.ErrRequestUnauthorized.WithHint("The consent request has expired, please try again.")) } return f, nil }
最適化を行わないコードと比較した場合の変更の影響を見てみましょう。
フローははるかに高速になり、データベースとのやり取りが減りました。
hydra_oauth2_flow
テーブルに新しいインデックスを導入することで、PostgreSQL のスループットを向上させ、CPU 使用率を減らすことができました。以下のスクリーンショットは、CPU 使用率が 100% に急上昇する改良されたインデックスを使用しないベンチマークの実行と、CPU 使用率が 10% 未満に留まる改良されたインデックスを使用したベンチマークの実行を示しています。
新しく追加されたインデックスにより、CPU 使用率 (緑色のバー) が削除され、バッファロックや関連する問題が発生する可能性が低くなります。
コードとデータベースの変更により、データベースへの合計ラウンドトリップが 4 ~ 5 倍 (行われたキャッシュの量に応じて) 削減され、データベースの書き込みが約 50% 削減されました。
次の仕様で Microsoft Azure 上の新しい実装をベンチマークします。
サービス | 構成 | 合計最大 SQL 接続数 | ノート |
---|---|---|---|
Ory Hydra 同意アプリ OAuth2 クライアント アプリ rakyll/hey (http ベンチマーク ツール) | 3x Standard_D32as_v4;米国中南部 5x Standard_D8s_v3;米国中南部 | 512 | すべての VM で、前述のすべてのプロセスが実行されました。 |
HA 構成の PostgreSQL 14 | メモリ最適化、E64ds_v4、64 仮想コア、432 GiB RAM、32767 GiB ストレージ。米国中南部 | | RAM が CPU を上回ります。 |
Ory は、上記の構成でピーク時に1 秒あたり最大 1090 ログイン、一貫して 800 ログインを実行できます。これは、フローをステートレスにし、頻繁に使用されるクエリのインデックスを最適化することで可能になります。
Ory チームが行ったパフォーマンスの最適化作業により、Hydra のパフォーマンスとスケーラビリティが大幅に向上しました。データベースへの書き込みトラフィックを削減し、コードベースと依存関係を改善することにより、Hydra はこれまでよりも高速かつ応答性が向上しました。インデックスを改善することで、Hydra はインスタンスの数に応じてより効率的にスケーリングできるようになりました。
今後も、さらに多くのトラフィックを処理できるよう、Ory のソフトウェアの最適化を継続していきます。データ モデルの最適化により、単一の PostgreSQL ノードで 5 倍のスループットを実現できると考えています。
OAuth2 サーバーを構築している場合は、Ory の完全に認定された OpenID Connect および OAuth2 実装を試してみることを強くお勧めします。 Ory OAuth2 –オープン ソースのOry Hydra をベースにした、グローバルな Ory Network 上で実行されるフルマネージド サービス– すでに説明されている最適化が利用されています。この記事を参照すれば、セットアップは数分しかかかりません。
ここでも公開されています。