こんにちは、マネーフォワード関西開発拠点のAPI推進部でインターンをしている進捗ゼミです。私たちのチームでは、マネーフォワードのプロダクトがサードパーティーアプリケーションとAPIを通じて連携するための仕組みを作成しています。私は、このチームで学んだことを活かして、Mini OAuth2 ProxyというOSSプロダクトを個人的に作成しました。この記事では、インターンの経験を交えながら、Mini OAuth2 Proxyについて紹介します。
私が所属しているAPI推進部では、アプリポータルというプロダクトを作っています。アプリポータルは、認証を担当するマネーフォワード IDと連携して、サードパーティーのアプリケーションがマネーフォワードのプロダクトと連携するためのOAuth2.0という認可の仕組みを提供するサービスです。
他にも、各プロダクトがOAuth2.0をスムーズに使えるように、トークンの検証を自動化するリバースプロキシを作ったり、APIの設計支援・移行支援をしたりしています。
私は2023年の12月からAPI推進部でインターンを始めました。グループで開発しているプロダクトを理解するため、また、趣味として、OAuth2.0およびOIDCについて学ぶことにしました。学習にあたって、以下のような文書を読みました。
これらの学習を終えた後、実用的なOIDCのRPの実装例を学ぶ目的で、OAuth2 ProxyというOSSを読むことにしました。結果的に、OAuth2 Proxyを読んだことは大いに役立ちました。しかし同時に、実装を読むことでOAuth2 ProxyがOAuthの学習に向いていない点も分かってきました。詳しくは後述しますが、OAuth2 Proxyは長年にわたって利用され続けてきた結果、巨大化しすぎてしまっているのです。私は、よりシンプルなことをよりうまくやる簡易版のOAuth2 Proxyが必要であると強く感じました。そのため、インターン生として学んできたことの集大成として、Mini OAuth2 Proxyの開発を行いました。
まずは、OAuth2 Proxyについて簡単に説明します。
OAuth2 Proxyは次の図にように動作します。
OAuth2 ProxyはリバースプロキシとしてOIDCによるログインを代行します。認証されていない状態で保護しているアプリケーションにアクセスしようとすると、次のような画面が出て、IdP(Identity Provider)へのログインを要求されます。(画像はOAuth2 Proxyの公式サイトより引用)
OAuth2 Proxyは、保護対象のアプリケーションに変更を加えることなくOIDCによる認証を提供できるため、次のような用途で使うことができます。
特定の人間だけで使いたいゲームサーバーを運用したいとします。不正アクセスを防ぎ、認証済みユーザーのみにアクセスを許可したいです。しかし、既存のゲームサーバーのコードを直接変更することは、複雑性や互換性の問題から現実的ではないことがあります。このとき、OAuth2 Proxyを活用することで、ゲームサーバーのアクセス制御を効果的に行うことができます。
上の図のApplicationをゲームサーバーに置き換えれば、ゲームサーバー自体のコードを変更せずに任意のIdPを使ったセキュリティ保護ができることが分かります。
マルチプロダクトを展開する企業にとって、ログインの仕組みを共通化することは重要です。例えば、マネーフォワードでは、数多くのプロダクトを提供しています。仮にそれぞれのプロダクトで個別のログインを求めた場合、ユーザーからすれば非常に面倒であると感じることでしょう。このような課題は、OIDCを活用したSSOで解決できます。例えば、マネーフォワードではマネーフォワード IDを導入しています。マネーフォワード IDは、マネーフォワードのプロダクトが利用しているIdPです。各プロダクトがマネーフォワード IDのRPになることによって、ユーザーはマネーフォワード IDにログインするだけで、関連するプロダクトをシームレスに利用できるようになります。これにより、各プロダクトで個別のログインを作成する必要がなくなり、ユーザーの利便性が大幅に向上します。OAuth2 ProxyはRPに必要な機能を実装しているOSSです。そのため、SSOの仕組みと組み合わせてOAuth2 Proxyを使うと、各プロダクトはログインの必要性を考える必要がなくなります。仮にマネーフォワードのSSOにOAuth2 Proxyを使った場合、次の図のように動作します。
注意:マネーフォワードではOAuth2 Proxyを使わずに自力でRPを実装しています。これはあくまでイメージ図です。
OAuth2 Proxyは便利なソフトウェアです。しかし、詳しく調べると欠点もあります。
OAuth2 Proxyには、実用上必要な機能がいくつか不足しています。
デバッグ用に大量のログを出力したいときも、重要な情報だけログを絞り込みたいときも、常に同じログを読むしかありません。結果、ログの解析が困難になっています。
プロキシ先のサーバーがリダイレクトを要求するレスポンスを返した場合に、適切にLocation
ヘッダーを書き換えないため、正しく動作しません。
OAuth2 Proxyは、自前で状態を保持する機能を持っていません。セッションで保持しておくべき情報を全てCookieまたは外部のRedisに保持しています。そのため、実装が非常に複雑になっています。例えば、OAuth2.0で定義されているstate
パラメータに通信に必要な情報を埋め込むなどのハックが含まれています。このような設計は可読性と保守性を大幅に悪化させています。
OAuth2 ProxyはProxyはOAuth2.0と本質的に関係しない機能をいくつも実装しています。以下は、その一部です。
robots.txt
の配信機能OAuth2 Proxyのコードベースは設計が洗練されているとは言えず、コードの理解が困難な部分があります。以下は、そうした例の一部です。
oauthproxy.go
に抽象化されるべき処理を直接書き込んでいるため、1,300行もあり全体像を理解することが難しいoauth2proxy.go
でも重要な処理をしているため、モジュール単体での理解が難しい/pkg/apis/middleware
と/pkg/middleware
、/validator.go
と/pkg/validation
といった同一の意味を持つように見えるモジュールが複数存在するこうした背景から、OIDCのRPの実装方法を理解する目的でOAuth2 Proxyを読むことは適切とは言えなくなっています。
Mini OAuth2 Proxyでは、OAuth2 Proxyの欠点を解消するべく、さまざまな工夫が施されています。
標準パッケージのlogは単に文字列を出力するにとどまっており、機能が不足していたため、ロギングライブラリを導入しました。ライブラリには、zerologを選定しました。ちなみに、API推進部でも同じライブラリを利用しています。
Location
ヘッダーを適切に修正実装しました。
シンプルなGo用のキャッシュが必要でした。そのため、キャッシュライブラリを導入しました。ライブラリには、go-cacheを選定しました。採用理由は、実装を読むのが簡単で、簡単に使うことができるためです。
欠点の項目で説明した、OAuth2.0と本質的に関係しない機能を全て削除し、OAuth2.0およびOIDCの仕様が定義している範囲だけを実装するようにしました。これにより、これまでOAuth2 Proxyが行っていた処理は全て別のコンポーネントによって行うことになります。シンプルな設計によって次のようなメリットが得られました。
Mini OAuth2 ProxyはシンプルなOIDCにおけるRPになりました。これにより、OAuth2.0およびOIDCの仕様に忠実な実装を行うことができるようになります。結果、実装上の疑問点を仕様を根拠に解決できるようになりました。例えば、Mini OAuth2 Proxyは、自力で暗号化を行なっていません。この設計の意図は、OAuth2.0の仕様に準拠することです。OAuth2.0の仕様では、通信は暗号化されなければならないと書かれています。しかし、実装方法は具体的に定義されておらず、単にTLSを使用しなければならないとだけ書かれています。そのため、OAuth2 Proxyでも、TLS終端機能を提供するリバースプロキシの背後で動作しなければらなないとだけ定め、具体的な実装をしないことで、より仕様に忠実に準拠しています。
さまざまな機能を大量に実装している関係上、OAuth2 Proxyの設定項目は多いです。設定項目の全貌は、公式ドキュメントのこの2つのページで書かれているのですが、一見しただけでもその数の多さがわかると思います。
これに対して、Mini OAuth2 Proxyの設定は次の2点を達成できているために、読みやすくなっています。
以下のjsonだけでも、設定可能な項目をほぼすべて網羅しています。
{ "oidc" :{ "providers":[{ "id": "IdP ID", "clientID": "CLIENT ID", "clientSecret": "CLIENT SECRET", "redirectURL": "REDIRECT URL", "startPath": "/start", "scopes":[ "SCOPE1", "SCOPE2"], "issuer": "ISSUER URL"}], "skipLoginPage":true}, "upstream" :{ "servers":[{ "id": "myservice", "url": "http://localhost:3000", "matchPath": "/myservice"}]}, "headerInjection":{ "request":[{ "name": "X-Authenticated-User", "type": "idTokenClaim", "values":[ "name"]},{ "name": "X-Authenticated-EMail", "type": "userInfo", "values":[ "email"]}], "response":[]}, "proxyURL":{ "host": "Mini OAuth2 Proxyよりもインターネット側で動作するTLS終端を行うプロキシのホスト名"}, "log":{ "level": "Info"}, "port":8080}
OAuth2 Proxyのコード量は35,000行程度です。
これに対し、Mini OAuth2 Proxyのコード量は1,600行程度まで減少しています。この程度のコード量であれば、簡単にすべての動作内容を理解することができます。
OAuth2 Proxyは様々な処理を実装しているので、大量のモジュールに依存してしまい、コードの読解が難しいです。例えば、github.com/pierrec/lz4/v4
というファイルの圧縮と解凍を行うモジュールをインポートしています。これは明らかにOAuth2.0の仕様とは関係のないモジュールです。OAuth2 Proxyが39モジュールに依存しているのに対して、Mini OAuth2 Proxyは6モジュールにしか依存していません。
OAuth2 Proxyの動作内容をもとにMini OAuth2 Proxyを再設計し、読みやすいコードを実現しました。この作業を行うには、設計の理論と方針を強く持つことが必要不可欠でした。なぜなら、OAuth2 Proxyの内容をそのまま使えば楽をすることはできますが、それはコードリーディングのコストをMini OAuth2 Proxyに押し付けることになるからです。そのため、まずはどうしてモジュールを分割するのか、どうしてモジュールの再利用をしてよいのかについて徹底的に考え、既存の設計原則を精密化する形で理論を構築しました。このときに考えた内容はQiitaのどうしてあなたの共通化は間違っているのかという記事にまとめたので、ぜひこちらも読んでみてください。再設計が完璧だとは思っていませんが、OAuth2 Proxyよりも適切な抽象化と関心の分離を行えたと考えています。
Mini OAuth2 Proxyは、OAuth2.0およびOIDCの仕様に忠実に実装されており、かつ、コード量が非常にコンパクトです。そのため、OAuth2.0およびOIDCの仕様と適切な実装を学ぶための教材として適しています。Mini OAuth2 Proxyのコードを読むことで、以下のような知識を得ることができます。
これらの知識は、RPの開発だけでなく、OAuth2.0およびOIDCに関連する様々なプロダクトの開発において役立つはずです。
Mini OAuth2 Proxyは、OAuth2 Proxyと同等の基本的な機能を提供しています。そのため、開発段階において、OAuth2 Proxyの代わりにMini OAuth2 Proxyを使用することができます。例えば、以下のようなケースで活用できます。
Mini OAuth2 Proxyは、OAuth2 Proxyと比べて、設定項目が少なく、設定ファイルの読み書きが容易です。そのため、開発段階での利用に適しています。また、Mini OAuth2 Proxyは、OAuth2.0およびOIDCの仕様に忠実に実装されているため、OAuth2 Proxyとは異なり、認証プロバイダの仕様変更の影響を受けにくいというメリットもあります。
これまで説明してきた通り、Mini OAuth2 ProxyはOAuth2 Proxyよりも優れた特性を持っています。そのため、以下のようなケースではOAuth2 Proxyの代替になりえます。
現在、Mini OAuth2 Proxyには次のような機能が不足しているため、開発を進めることで実際に使われるOSSにしていきたいと思っています。
今回紹介したMini OAuth2 Proxyはまだまだ完成したソフトウェアではありません。コントリビューションを歓迎しています。私は、これまでもプログラミングには親しんできましたが、本格的なOSSを公開したのは初めてなので、大きな達成感を感じています。OAuth2 Proxyのコードを読むのはかなり根気が必要な作業でしたが、インターン先で使われている技術であるということが大きなモチベーションになり、やりきることができました。マネーフォワードでのインターンは、直接的に技術を学べるだけでなく、技術が実際に活用されている現場を見ることができるため、技術を突き詰めていく原動力としても大きな助けになると思います。Mini OAuth2 Proxyが誰かの役に立つことを願っています。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。