はじめに
今から1つ大きな脆弱なポイントがあるアプリケーション(フィクション)の設計を紹介します。どこに問題があるか考えながら読んでみいただけると楽しめるかもしれません。
事業会社のCIOとベンダー企業のある話。
注 : この話はフィクションです
CIO: 「ウチのECサイトにAuth0を導入しようと思う」
ベンダー: 「Auth0ですか、最近Oktaに買収されたIDaaSですよね」
CIO: 「そーそー、これを機にセキュリティリスクを転嫁してしまった方がええんやと思う。ということで認証システムのリプレースよろしく」
ベンダー: 「はい!!!」
数ヶ月後
ベンダー: できました!
CIO: 「おー、じゃあテキトーにシーケンス図でも見せて」
ベンダー: 「はい、ではまずログインのところから。現在SPAで構築されているWebアプリケーションを認可コードフロー+PKCEで認証しトークンを取得します。今までの認証方式からOAuth2.0に準拠した安全な認証方式に作り替えます。」
※ Webアプリケーションはフロントエンドorクライアントと読み替えていただいても大丈夫です。
Auth0のログイン
CIO: 「モダンでええやないか!ちなみにこれって前から使ってたユーザーがいるだろう?その認証情報との紐付けはどうするつもりだ?」
ベンダー: 「はい、そちらは大丈夫です。Auth0にログインしてもらったのちに以前から登録されていたメールアドレスと照合し、そこにワンタイムパスワードを送信することで所持情報による本人確認を実施。Auth0のuser_idとアプリケーションで管理しているidの紐付けを実施します。」
旧認証方式との統合
上記のログイン成功後より
CIO: 「ほお、なかなか厳重そうだの。その後の操作はどうなる?」
ベンダー: 「まず、ログイン以降のAPIの認可はAuth0のアクセストークンを用いて行います。また、基本的にはテーブル構造は変更していないので既存のアプリケーションで管理しているユーザーidを使って注文情報のCRUD操作を行えるようになっています。」
アプリケーションAPIの利用
CIO: 「全体として良さそうだね。あとはいい感じによろしく」
この設計について
いかがだったでしょうか?この設計に大きな脆弱なポイント見つけれたでしょうか?
全体として割としっかりセキュリティについて考慮されている雰囲気はありつつ何点かツッコミポイントを用意してあります。無論これから話すことはあくまで一意見なのでそれ以外にも軽微な問題点はあると思いますが一点明らかにマズいだろうというのが存在しているので今回はそれ以外の問題点については目をつぶっていこうと思います。
Auth0ログインのレビュー
アーキテクチャを再掲しておりますが、ここの設計は大きな問題点はないです。OAuth2.0に準拠した認可コードフロー+PKCEの構成になっておりユーザー認証に関しては概ね問題ない構成になっています。
強いていうなら stateを絡ませてCSRFの対策はできた方が良かったですね。
ただ、正直この辺に関しては最近は便利なクライアント用のSDKをAuth0が出してくれているのでそれを利用すればその辺はほとんど心配いらないです。
ちなみにこの辺のフローをクライアント側を自前で作りたい場合もAuth0がAPIを提供してくれているので可能です。
旧認証方式との統合のレビュー
旧認証方式との統合も大きな問題はないと思います。Auth0のIDトークンはemailでログインした場合メールアドレスを含んでいるのでIDトークンをデコードすればペイロード部分からメールアドレスを取得できます。また、その際にjwtの署名の検証もすることでそのトークンの改ざんを検知できます。また、そのメールアドレスにワンタイムパスワードを送信することでメールアドレスの所持情報を確認することができるので、他人のメールアドレスとpwでAuth0のアカウントを新規作成して既存のアプリケーションのユーザー情報と紐付けられるという不正を防止することもできます。
悪意ある第3者が不正に利用しようとした場合の図
ちなみにAuth0アカウントで新規アカウント作成の時にメールを送って、メールアドレスの所持情報が確認できないとアカウントを作成させないようにすることも可能です。(多分そっちの方がいい)
また、この辺の統合に関しては Auth0 Actions を活用するといい感じにできますがそれは別の機会にでも気が向いたら紹介します。
アプリケーションAPIの利用のレビュー
問題はここです。察しの良い方は認識の通りですが認可が適切にできていないというのが問題でした。
Auth0でログインし、既存のアプリケーションと統合するところまでは問題はありませんでした。ただ、その後トークンとユーザーidをクライアントから渡すというのが問題です。
具体的には以下のケースが考えられます。
このケースの場合、トークンは攻撃者であるid:1のもの。しかし、APIでユーザーのidとして認識しているはリクエストボディに含まれるidです。なのでこのidを変えてしまいさえすれば誰にでも成りすませてしまうのです。
何が悪かったか
ジャンル分けで問題点を整理していこうと思います。
権限管理
今回、Auth0のアクセストークンを使ってAPIの認可をしていました。しかし、このケースでは「トークンさえあればどのユーザーの情報でも操作ができる」という 誰でもAdmin 状態になってしまっていました。
マイグレーション
「普通こんな実装するわけないだろ」と思うエンジニアもいると思いますが確かに普通はそうです。ただ、今回は旧ECサイトから認証をAuth0にマイグレーションするという課題が発生しました。今後、Auth0がメジャーになっていくにあたってどんどんこういった要件は増えていくかとも思います。その時に、Auth0に認証すれば適切に認可ができる状態というのを構築する必要があります。権限管理で挙げた課題であるidをリクエストボディに含めないようにしたとしたら、アクセストークン一つで「このユーザーが誰なのか?」という識別をできるようにしなくてはいけません。
余談ですが、認可とは認証が済んでいることが前提で成り立つ仕組みです。なので認可の時には誰が認証したんだっけってわかるような最低限の情報が必要なわけなので、トークンのペイロードの中にidかそれに相当する個人の識別子が最低限含まれているわけです。
組織的なところ
「Auth0を導入すればセキュリティはバッチリで責任を転嫁できる」みたいな考えも間違ってはいないと思います。確かに、id,pwを自前のDBで保管する必要はなくなるのでリスクは減ります。ただ、Auth0は顧客のすべての認証認可の責任を担保するサービスではないです。現状サイトがあるのであれば、認証や認可に関してどのような課題があり、それをクリアするにはどのような機能があれば解決できるか?といった要件を整理した上でAuth0を選ぶべきだったでしょう。
これを助言できるシステムも事業も良くわかっている人材が大事なのかもですね
問題の解決策
組織的な問題は置いておいて、Auth0を利用しどのようにAPIで適切な認可ができるようになるか?というのを主観に2つの解決策の設計例を提示しようと思います。
そして双方に共通することとして肝としては「どのようにAuth0で発行されるuser_idとアプリケーションで管理していたユーザーidを結びつけるか?」ということになります。
アプリケーション管理のDBにあるAuth0との紐付きテーブルを活用する
現状のマイグレーションの方式であればDBのテーブルにAuth0のuser_idとアプリケーションで管理されている既存のユーザーidの紐付きテーブルが存在しています。
つまり、Auth0のアクセストークンがリクエストされる度にトークンの署名の検証をし、そのテーブルを参照してuser_idからアプリケーションで利用しているidをAPI側で用意してやればクライアント側のなりすましを防ぐことができます。
メリット
- WebアプリケーションとAPIの改修だけで対応できる
- 紐付きの管理がしやすい
デメリット
- 認可を判断するためにDBの紐付き情報を確認する必要があるので、そのテーブルにアクセスできるAPIしか認可できなくなる。
- クエリの結合条件を変更する必要がある
Auth0にアプリケーションのユーザーidを持たせる
これはAuth0のアクセストークンのペイロード内に既存のアプリケーションのユーザーidを持たせてしまうという案です。Actions という機能と ManagementAPI を活用すれば可能です。
Auth0にemialとpwでユーザーに新規登録してもらい、emailの所持情報が確認できたらAuth0の対象ユーザーのapp_metadataという領域にアプリケーションのユーザーidを保存します。
その後Actionsの設定を追加することでアクセストークンの中にユーザーidが渡されてくるようになるのでそれ以降はトークンの検証だけでAPIの認可ができるようになります。
メリット
- 認可を判断するためにDBの紐付き情報を確認する必要がないので、そのテーブルにアクセスできないAPIでも認可ができるようになる。
- Auth0でコンソール内でのActionsの実装や設定の変更が必要になる。
デメリット
- 紐付きの管理がしにくい
- 認可の仕組みを変更するので
余談
「この設計方法って普通思いつくよね?」て思ったエンジニアの方もいるかもしれませんが確かにそうです。ただ現場で大いに起こりうる過ちだとも思っています。なぜならIFの大きな変更は不安でリスクも大きいからです。「リクエストボディにパラメータ増やす」とかであれば全然簡単ですが、「ヘッダーにトークンを付与させてその署名の検証をして中からidを抽出してテーブルと突合させる」となるとその実装だけで大変ですし、認可の仕組みが大きく変わることで自動テストの修正が発生する可能性がある、ほぼすべてのクエリも修正する必要がある。そういった実装による障害発生のリスク回避をしようと結果セキュリティリスクを発生させ未来に負債を残すなんて話は聞いたことが、、、あるかもしれません。
Auth0 は認証認可の銀の弾丸ではない
Auth0とは認証と認可をアプリケーションに簡単に実装できるようにするためのサービスで、俗にIDaaSと呼ばれるサービスの一種です。自前でそれら全ての認証・認可の仕組みを構築する頃に伴う時間やコスト、リスクを避けることができます。
Auth0 is a flexible, drop-in solution to add authentication and authorization services to your applications. Your team and organization can avoid the cost, time, and risk that come with building your own solution to authenticate and authorize users.
つまり、あくまでシステム構築の便利なツールなので
⭕️ Auth0を使って認証・認可の仕組みをいい感じに作ることができる
❌ Auth0を使えばいい感じに認証・認可の仕組みが出来上がる
というわけです。
最後に
Auth0は優れたIDaaSのサービスです。ただ、AWSやfirebaseといったサービスと一緒でどう扱うかで本当に良くも悪くもなります。使えば良いというものではありません。
だからこそ正しく使える方法を考える機会を作っていきたいと思い今回の記事を作成しました。
また、今後も Auth0 をいじって色んなナレッジを貯めていきたいと思いますので設計や実装で相談などある方いたらご連絡ください。