はじめに
こんにちは、マネーフォワード関西開発拠点のAPI推進部でインターンをしている進捗ゼミです。 私たちのチームでは、マネーフォワードのプロダクトがサードパーティーアプリケーションとAPIを通じて連携するための仕組みを作成しています。 私は、このチームで学んだことを活かして、Mini OAuth2 ProxyというOSSプロダクトを個人的に作成しました。 この記事では、インターンの経験を交えながら、Mini OAuth2 Proxyについて紹介します。
TL;DR
- OAuth2 ProxyはOIDC(OpenID Connect)による認証を簡単に追加することができる便利なOSSです
- OAuth2 Proxyは複雑で、簡易的な用途や学習用には適しません
- Mini OAuth2 Proxyが問題を解決します
どうしてMini OAuth2 Proxyを作ったのか
API推進部の紹介
私が所属しているAPI推進部では、アプリポータルというプロダクトを作っています。アプリポータルは、認証を担当するマネーフォワード IDと連携して、サードパーティーのアプリケーションがマネーフォワードのプロダクトと連携するためのOAuth2.0という認可の仕組みを提供するサービスです。
他にも、各プロダクトがOAuth2.0をスムーズに使えるように、トークンの検証を自動化するリバースプロキシを作ったり、APIの設計支援・移行支援をしたりしています。
マネーフォワードに入って学んだこと
私は2023年の12月からAPI推進部でインターンを始めました。グループで開発しているプロダクトを理解するため、また、趣味として、OAuth2.0およびOIDCについて学ぶことにしました。学習にあたって、以下のような文書を読みました。
- OAuth徹底入門 セキュアな認可システムを適用するための原則と実践:初めの一歩として読みやすい本でした。前半の部ではサンプルコードと合わせて、実装を詳しく解説してくれるので理解が深まりました。
- 標準仕様の日本語訳:困ったら常にここに立ち戻って参照する辞書として使いました。最初からこれを読むと心が折れますが、ある程度の仕組みを理解してから通読すると一気に理解度が上がりました
- 標準ライブラリのoauth2実装:Goの標準ライブラリはシンプルに実装されているため、読みやすかったです
- coreos/go-oidc:標準ライブラリではありませんが、広く使われているOIDCのRP(Relying Party)を実装するためのライブラリです。かなり読みやすかったです
- マネーフォワードの製品の実装:マネーフォワードでは、インターン生でも業務時間外に業務のコードを読んで勉強することができます。重要な部分はアーキテクチャにも気を使って作られているので、かなり役立ちました
OAuth2 Proxyとの出会い
これらの学習を終えた後、実用的なOIDCのRPの実装例を学ぶ目的で、OAuth2 ProxyというOSSを読むことにしました。結果的に、OAuth2 Proxyを読んだことは大いに役立ちました。しかし同時に、実装を読むことでOAuth2 ProxyがOAuthの学習に向いていない点も分かってきました。詳しくは後述しますが、OAuth2 Proxyは長年にわたって利用され続けてきた結果、巨大化しすぎてしまっているのです。私は、よりシンプルなことをよりうまくやる簡易版のOAuth2 Proxyが必要であると強く感じました。そのため、インターン生として学んできたことの集大成として、Mini OAuth2 Proxyの開発を行いました。
OAuth2 Proxyを使って簡単に認証機能を導入しよう!
まずは、OAuth2 Proxyについて簡単に説明します。
アーキテクチャ
OAuth2 Proxyは次の図にように動作します。
OAuth2 ProxyはリバースプロキシとしてOIDCによるログインを代行します。認証されていない状態で保護しているアプリケーションにアクセスしようとすると、次のような画面が出て、IdP(Identity Provider)へのログインを要求されます。(画像はOAuth2 Proxyの公式サイトより引用)
使われる場面
OAuth2 Proxyは、保護対象のアプリケーションに変更を加えることなくOIDCによる認証を提供できるため、次のような用途で使うことができます。
仲間内のゲームサーバー
特定の人間だけで使いたいゲームサーバーを運用したいとします。不正アクセスを防ぎ、認証済みユーザーのみにアクセスを許可したいです。しかし、既存のゲームサーバーのコードを直接変更することは、複雑性や互換性の問題から現実的ではないことがあります。このとき、OAuth2 Proxyを活用することで、ゲームサーバーのアクセス制御を効果的に行うことができます。
上の図のApplicationをゲームサーバーに置き換えれば、ゲームサーバー自体のコードを変更せずに任意のIdPを使ったセキュリティ保護ができることが分かります。
RP実装の簡略化
マルチプロダクトを展開する企業にとって、ログインの仕組みを共通化することは重要です。例えば、マネーフォワードでは、数多くのプロダクトを提供しています。仮にそれぞれのプロダクトで個別のログインを求めた場合、ユーザーからすれば非常に面倒であると感じることでしょう。 このような課題は、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は便利なソフトウェアです。しかし、詳しく調べると欠点もあります。
必要な機能が足りていない
OAuth2 Proxyには、実用上必要な機能がいくつか不足しています。
ログレベルが無い
デバッグ用に大量のログを出力したいときも、重要な情報だけログを絞り込みたいときも、常に同じログを読むしかありません。結果、ログの解析が困難になっています。
リダイレクトができない
プロキシ先のサーバーがリダイレクトを要求するレスポンスを返した場合に、適切にLocation
ヘッダーを書き換えないため、正しく動作しません。
状態を保持できない
OAuth2 Proxyは、自前で状態を保持する機能を持っていません。セッションで保持しておくべき情報を全てCookieまたは外部のRedisに保持しています。そのため、実装が非常に複雑になっています。
例えば、OAuth2.0で定義されているstate
パラメータに通信に必要な情報を埋め込むなどのハックが含まれています。このような設計は可読性と保守性を大幅に悪化させています。
OAuth2.0と本質的に関係しない機能が多い
OAuth2 ProxyはProxyはOAuth2.0と本質的に関係しない機能をいくつも実装しています。以下は、その一部です。
- 静的ファイル配信機能
- 認証の結果得られたUserInfoを取得するエンドポイント
robots.txt
の配信機能- 認証の可否を取得するエンドポイント
- Basic認証機能
- IPアドレスによる認証機能
- 一部のパスの認証を省略する機能
設計に改善の余地がある
OAuth2 Proxyのコードベースは設計が洗練されているとは言えず、コードの理解が困難な部分があります。以下は、そうした例の一部です。
- トップレベルのモジュールである
oauthproxy.go
に抽象化されるべき処理を直接書き込んでいるため、1,300行もあり全体像を理解することが難しい - モジュールを分割しているが、
oauth2proxy.go
でも重要な処理をしているため、モジュール単体での理解が難しい - モジュールの分け方のルールが一貫していない
- 例えば、
/pkg/apis/middleware
と/pkg/middleware
、/validator.go
と/pkg/validation
といった同一の意味を持つように見えるモジュールが複数存在する
- 例えば、
こうした背景から、OIDCのRPの実装方法を理解する目的でOAuth2 Proxyを読むことは適切とは言えなくなっています。
Mini OAuth2 Proxyの解決策
Mini OAuth2 Proxyでは、OAuth2 Proxyの欠点を解消するべく、さまざまな工夫が施されています。
不足している機能を追加
構造化ロガーを導入
標準パッケージのlogは単に文字列を出力するにとどまっており、機能が不足していたため、ロギングライブラリを導入しました。 ライブラリには、zerologを選定しました。ちなみに、API推進部でも同じライブラリを利用しています。
リダイレクトのLocation
ヘッダーを適切に修正
実装しました。
自前で状態を保持できる機能を実装
シンプルなGo用のキャッシュが必要でした。そのため、キャッシュライブラリを導入しました。 ライブラリには、go-cacheを選定しました。採用理由は、実装を読むのが簡単で、簡単に使うことができるためです。
OAuth2.0に関係しない機能を削除
欠点の項目で説明した、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つのページで書かれているのですが、一見しただけでもその数の多さがわかると思います。
- https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview
- https://oauth2-proxy.github.io/oauth2-proxy/configuration/alpha-config
これに対して、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のコードを読むことで、以下のような知識を得ることができます。
- OAuth2.0およびOIDCの仕様で定義されている各エンドポイントの役割と動作
- RPとしてOAuthおよびOIDCを実装する際の、Goにおけるベストプラクティス
- OAuth2.0およびOIDCの仕様に準拠しつつ、RPとしての機能を提供するためのアーキテクチャ・設計
これらの知識は、RPの開発だけでなく、OAuth2.0およびOIDCに関連する様々なプロダクトの開発において役立つはずです。
開発段階での簡易版OAuth2 Proxyとしての活用
Mini OAuth2 Proxyは、OAuth2 Proxyと同等の基本的な機能を提供しています。そのため、開発段階において、OAuth2 Proxyの代わりにMini OAuth2 Proxyを使用することができます。例えば、以下のようなケースで活用できます。
- Redisを使わずにローカル環境でOAuth2.0およびOIDCの動作を検証したい
- 趣味のプロダクトで、簡易的な認証・認可機能を提供したい
Mini OAuth2 Proxyは、OAuth2 Proxyと比べて、設定項目が少なく、設定ファイルの読み書きが容易です。そのため、開発段階での利用に適しています。また、Mini OAuth2 Proxyは、OAuth2.0およびOIDCの仕様に忠実に実装されているため、OAuth2 Proxyとは異なり、認証プロバイダの仕様変更の影響を受けにくいというメリットもあります。
将来的なOAuth2 Proxyの一部代替
これまで説明してきた通り、Mini OAuth2 ProxyはOAuth2 Proxyよりも優れた特性を持っています。そのため、以下のようなケースではOAuth2 Proxyの代替になりえます。
- OAuth2 Proxyの最低限の機能だけを使っているプロジェクト
- 新規プロダクトでのRP実装の省略用での導入
現在、Mini OAuth2 Proxyには次のような機能が不足しているため、開発を進めることで実際に使われるOSSにしていきたいと思っています。
- テストによる品質保証:OAuth2 Proxyに比べて動作の実績が不足しています
- Redis対応:go-cacheによるキャッシュは非常にシンプルです。しかし、スケーラビリティや可用性の面で、Redisには劣ります。必要に応じて状態の保存先を切り替える機能を実装する必要があります。
おわりに
今回紹介したMini OAuth2 Proxyはまだまだ完成したソフトウェアではありません。コントリビューションを歓迎しています。 私は、これまでもプログラミングには親しんできましたが、本格的なOSSを公開したのは初めてなので、大きな達成感を感じています。OAuth2 Proxyのコードを読むのはかなり根気が必要な作業でしたが、インターン先で使われている技術であるということが大きなモチベーションになり、やりきることができました。マネーフォワードでのインターンは、直接的に技術を学べるだけでなく、技術が実際に活用されている現場を見ることができるため、技術を突き詰めていく原動力としても大きな助けになると思います。 Mini OAuth2 Proxyが誰かの役に立つことを願っています。