まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:react

最近関わったプロジェクトの中で「ウェブアプリケーションにユーザー管理機能/ログイン機能を付与する」ことを検討する機会がありました。これ自体は特別珍しいことではないと思っています。


こういったユーザー管理機能を実装しようとした場合、例えばログイン単体機能だけを考えればいい場合はデータベースに users テーブルを用意して、ログイン画面を作って認証、、とすればすぐに実現できそうです。が、現実的にはユーザー登録していない人向けにオンラインサインアップ機能を付けたり(指定されたメールアドレスにメールを送信して、そのメール内のリンクからユーザー登録の続きを行う、ということになるのでメール送信機能が必要になります)、パスワード変更を望むユーザーのための画面を作ったり、パスワードを忘れてしまった人のためのリセット機能を作ったり(これもメール送信が必要になりますね)、、それほど単純な機能追加だけにとどまらないことになります。加えて後述の「開発者を信用していいか?」という問題にも関わることになります。詳しくは以下でも紹介しますが、セキュリティ面まで考慮するとユーザー管理機能は「自前で用意すべきではない」機能の1つだと思っています。

というわけでアプリケーションにユーザー管理機能/ログイン機能を付与する場合の選択肢としては「専用サービスを使う(専用サービスの認証機能を自分のアプリケーション内に組み込む)」のが現実的ではないかと思っています。上述に挙げたような面倒そうな機能が一通り準備された状態で使うことができ、OAuth 2.0 をベースとしたセキュリティ要件もかなり高く設定できます。具体的には Auth0IBM AppIDAmazon CognitoGoogle Firebase などが候補になると思っています(以後、これらのサービスのことを ID as a Services という意味で "IDaaS" と呼ぶことにします)。

一方で、ログイン画面はそのアプリケーションの一部となるので、当然のように「ログイン画面をアプリケーションと(統一された CSS やヘッダー、フッダーという意味で)同じデザインにしたい」という要求もでてきます。各種 IDaaS には初めから用意されたログイン画面というものもありますが、その画面をそのまま使うのではなく、カスタマイズされたログイン画面からログインしつつ、API を使って認証機能そのものは IDaaS のものを使う、というものです。具体的な実現手順に関しては組み込み先のアプリケーションで使われているフレームワークや IDaaS によって異なりますが、OAuth 2.0 を使った SDK や API によって実現そのものは可能なものばかりです。自分も以前に IBM AppID を使ってログイン画面のカスタマイズを紹介したこともありました。


さて本ブログエントリのテーマとなる問題はここからです。IDaaS を使って自分のアプリケーションにログイン機能を組み込んだり、その画面をカスタマイズしたりすること自体は可能そうに思えるのですが、ここに「セキュリティ」という新たな要素を含めて考えてみます。一言でセキュリティと言ってもその観点はいくつもあるので「どういった観点で考えた時にどういうケースを心配する必要があるか?」を分けて考える必要があります。要は単純に1つの答にたどり着くのが難しく、ケース・バイ・ケースで考えないといけないような結構面倒な話なのでした。作成しているアプリケーションの特性や公開範囲、優先事項とその順位も考慮した上で「何をどこまで妥協できるか?」という観点でも考える必要があると思っています。以下、その優先事項の例を挙げつつ、何を優先するとどういった影響が起こりうるか、という視点をかいつまんで紹介していきます。なお、以下の説明では IBM AppID を使った場合で説明しますが、他の IDaaS を使っている場合も同様と考えてください。


【フロントエンドフレームワーク】
まずはクラウドのメリットを活かしやすい、と言われるフロントエンドフレームワーク(React, Angular, Vue など)を使ってウェブアプリケーションを開発しているか? という観点を考えてみます。このフロントエンドフレームワークを採用することでサーバーサイドにアプリケーションサーバーが不要になり(HTTP サーバーでよくなり)、負荷軽減やセキュリティ脆弱の可能性が下がるというメリットがあります。

一方でフロントエンドに全てのリソースを用意する必要があるため「アプリケーションを構成している要素(HTML や JavaScript)が全てウェブブラウザ経由で見えてしまう」という制約があります。IDaaS を使った認証ではこの制約が問題になるケースを考慮する必要があります。

より具体的な内容を説明します。たとえば通常の(フロントエンドフレームワークを使わない)サーバーサイドアプリケーションで AppID を画面カスタマイズ無しで認証機能として使う場合(サーバーサイド SDK を使う場合)、AppID から以下の情報を取得する必要があります:
  • リージョン(IDaaS インスタンスのあるロケーション)
  • テナントID(IDaaS インスタンスを特定する ID)
  • クライアントID(アプリケーションを特定するID)
  • シークレット
  • コールバック URL

実装手段にもよりますが、サーバーサイドサイドアプリケーションではこれらの情報を外部に開示する必要はなく、認証に関わる処理を全てサーバー側で行うように設計することでこれらの情報を外部から参照できないように作ることができます。重要性にランクを付けるつもりはありませんが、特にこの中でもシークレットと呼ばれる情報は(パスワードに相当するもので)機密性が高く、特定の人だけが知りうるように運用する必要があるものです。

一方でフロントエンドフレームワークを使ったアプリケーションから AppID の認証機能を使う場合(フロントエンド向け SDK を使う場合)、AppID から以下の情報を取得する必要があります:
  • クライアントID(アプリケーションを特定するID)
  • エンドポイント URL

サーバーサイドの時と比べて必要なパラメータ数は少なくなります。これは認証に用いるプロトコルが異なるためです(OAuth 2.0 コールバックと、OAuth 2.0 PKCE の違い)。ただフロントエンドフレームワークの場合はサーバーサイドの場合と異なり、これらのパラメータを全て JavaScript 内に記載する必要がある、という点が異なります。実際のプログラミング時ではこれらのパラメータを全て環境変数化してコードとは分離するのが一般的ですが、フロンドエンドアプリケーションとしてビルドする際に同環境変数も取り込まれ、静的なファイルとなった中にはプレーンテキストで含まれることになります(つまりプレーンテキストで外部公開されます)。

「とはいえサーバーサイドの時と違ってシークレットが不要だから(つまりシークレットは公開されないから)いいのでは?」と考えることもできます(実際、そのような考えでこの仕組みが提供されています)。たしかに最も機密性の高いシークレットが開示されるわけではない、と考えることはできますが、一方で「OAuth 2.0 PKCE ではこれらの情報だけで SDK を利用することができる」わけです。つまり「このアプリケーションの認証機能を実装する上で必要になる情報全て」が公開されている、とも言えます。

これはちょっと怖い事実といえます。現実的にはベンダー側(AppID 側)の設定でログイン URL を指定したりすることで情報の不正利用を防くことができるのですが、(詳しくは書きませんが)悪さをする側でも更に対策の対策のようなことができたりして、これだけでも好ましくなかったりします。なお、この件は後述の画面カスタマイズの時にも関わってくる、ということを先に記載しておきます。

ここで言いたかったのは「フロントエンドフレームワークには運用上の大きなメリットはあるが、認証セキュリティの件では通常のウェブアプリケーションよりも低くなってしまう」ということです。「低いからダメ」とまでは言いませんが、どういうことが起こり得て、それを正しく理解した上でそれは許容できるものかどうかを判断する必要があるもの、ということになります。


【認証画面のカスタマイズ】
上述の「フロントエンドフレームワークを採用するか否か」とは別に、認証画面を(ベンダー提供の標準画面を使うのではなく)自分達でカスタマイズしたものを使いたいかどうか、という観点を考えてみます。他の条件が変わらないのであれば、当然ログイン画面も(ベンダーから提供されるものではなく)アプリケーションテーマに合うようなものにカスタマイズしたくなると思っています。

ただこのカスタマイズには技術的なものとは異なる大きな壁があります。ログイン画面を開発するということは、入力したユーザー ID やパスワードをサーバーに送って、、、という処理も伴うわけですが、この処理部分を担当するアプリケーション開発者を信用してよいのか? という壁です。

「え?でもログイン画面を実装するなら当然じゃないの?」と考える人もいると思うので補足しておきます。上でも書いた OAuth 2.0 のコールバックや PKCE といったプロトコルを使い、画面カスタマイズなしで(ベンダーが用意したログイン画面を使って)認証する場合、利用者が入力したユーザー ID やパスワードをサーバーに送る、という処理は認証サービス側で用意することになります。つまりアプリケーション開発者からは手が出せない処理ということになります。この方法であれば利用者が入力した ID やパスワードが正しいかどうかを判断することも、正しかった場合に正しかったという結果を含めて次の処理に移ったり(リダイレクトしたり)、またオンラインサインアップやパスワードリセット時の処理に関しても同様にベンダーが用意済みの処理をそのまま利用することになるので、アプリケーション開発者からは手を出したくても出せない処理部分ということになります。したがってアプリケーション開発者は悪いことをしない、という前提に立つ必要もないのでした(IDaaS ベンダー側を信用する必要はありますけど、それはカスタマイズする場合も同様です)。


これがログイン画面を自分達で用意する場合、確かにユーザーインターフェース的にはアプリケーションに合ったものが用意できるというメリットがある一方、アプリケーション開発者が(例えば正しいことが分かった ID とパスワードを記録する、といった)処理を行う余地が残ってしまいます。これが「アプリケーション開発者を信用してよいか?」という壁になるのです。 上述した「ユーザー管理機能は自前で用意するべきではない」といったのはこのような背景があってのものでした。性善説に基づいて考えれば、そんな悪い開発者ばかりではないでしょうし、社内の者がそんなことをするメリットもない、と考えることはできます。ただ「ではそういう設計でよいか?」というのは別の問題であって、アプリケーションのセキュリティ設計の責任を持つ立場の人が考えることになると思っています(繰り返しますが「絶対ダメ」ではないのです。アプリケーションの特性やメリット・デメリット・優先順位を考慮した上で最終的には個別に判断するべきものだと思っています)


少しややこしい話をします。この「開発者を信用する/しない」という問題は、上で紹介した「フロントエンドフレームワークを使った場合、同じ認証機能を別のアプリで実装するために必要な情報が全てインターネット上に公開されてしまう」という問題と絡めて考えるべきとも思われます。「開発者を信用できるならよい」だけかもしれなかった話が、「信用できない開発者が作ったかもしれないログイン画面」を許すかどうかという問題も併せて考える必要が出てきてしまうのでした。


なお IDaaS を使わずに認証機能を自前で用意する場合に関しては選択肢がなく、「開発者を信用しないといけない」ことになります。試験的なアプリケーションであればこれが大きな問題とはならないと考えることもできますが、コンシューマー向けに公開して個人情報を集めるようなアプリケーションの場合、その設計は考え物だと思っています。


ここまでに紹介したようにアプリケーションの認証は「フロントエンドフレームワークを採用するか」、「ログイン画面のカスタマイズをするか」という2つの軸だけで考えても結構複雑な問題となります。現実は更にアプリケーションの特性(ユーザーは誰なのか? どの程度の負荷が想定され、どの程度の安定性が求められるか? 運用予算はどの程度か? など)も絡めて考慮する必要があるので、明確な基準を作るのが難しく、どうしても個別判断が多くなってしまうと思われます。


【フロントエンドフレームワークで IDaaS を使って画面をカスタマイズする場合】
では(ある程度のリスクを承知の上で)「フロントエンドフレームワークを使って開発するウェブアプリで、IDaaS を使ってユーザー管理を行うようなケースで、ログイン画面もカスタマイズしたい」という決定がなされた場合(作らないといけない立場になった場合)、どのような点に気を付けて実装すべきでしょうか。

※アプリをゼロから作るケースで、もし自分に設計の決定権があった場合は、おそらくこのような決定はしないと思う、ということは最初に言っておきます。わざわざフロントエンドでは作らない、たぶん。。


まず上で書いたような理由があるため、ログイン画面自体はサーバーサイドアプリケーションとして(フロントエンドフレームワークのアプリケーションとは別に)用意するべきです。そして フロントエンド → ログイン画面 → 認証 → ログイン画面で結果を取得 → フロントエンドに結果を返す  といった(リダイレクトによる)連携を行う設計にします:
img01


ここで特に気を付けるべきは最後の「ログイン画面で結果を取得 → フロントエンドに結果を返す」部分です。フロントエンドは HTTP GET リクエストしか受け付けることができないので、リダイレクト時に何か情報を渡そうとすると GET リクエストのパラメータで渡すしかありません。そのパラメータのフォーマットが推測可能な形になっていると認証を回避できてしまう可能性もあります:
img02


一例としてはこのような方法が考えられます。フロントエンドからログイン画面にリダイレクトする前に乱数を使ってランダムな値を生成し、その値を保持した上で、そのランダムな値をパラメータにログイン画面へリダイレクトします(ログイン画面でも同じ値を保持させます)。その上で IDaaS に対して認証を行い、ログインが成功した場合はログイン成功を示す情報(ユーザーIDなど)を保持していたランダム値で暗号化した上で、結果の暗号化文字列を URL パラメータに含めてフロントエンドに戻ります。最後にフロントエンドは自分が保持していたランダム値を使ってパラメータを復号化してユーザー情報を取り出す、といった具合です。この方法でもフロントエンド側で値を保持する必要があるので、そこで localStorage や sessionStorage などが必要になり、使わない場合と比べてセキュリティリスクが高くなってしまうのですが、一方でこの方法であればログイン認証を回避して(認証結果だけを偽造して)ログインしたかのように見せかける手法は使えず、また URL の一部にパラメータが表示されることがあってもその内容は暗号化されているので、ぱっと見ではその内容が推測できない、という要件はクリアできます。この方法で充分なセキュリティ要件を満たしているかどうかはアプリケーション次第の所もあり、個別に判断する必要があります。が、「この方法でも大丈夫」と判断されるケースであれば、このようなやり方もある、という例として紹介しました:
img03


【サンプルコード】
こういうケースで実装しなければならなくなった場合の1つのケースとして紹介しました。IDaaS やフロントエンドフレームワークの種類によって実装の実現方法も異なるし、ログイン画面のアプリとして作るウェブアプリのパフォーマンスも変わってくるとは思うのですが、仮に IDaaS として IBM AppID を、フロントエンドフレームワークとして ReactJS を使う場合のサンプルソースコードを作って公開してみたので紹介しておきます。Auth0 などでも同様に使えると思います:
https://github.com/dotnsf/appid-spa-customlogin


まず "git clone" などで上記 URL からソースコードを取得します。中には "spa", "web", "spa-normal" という3つのフォルダがあり、それぞれがアプリケーションになっていますが、サンプルとして使うのは "spa" と "web" の2つだけです:
2023031901


なお、spa-normal/ は React から AppID をカスタマイズ無しで使う場合のサンプルとなっています。こちらをセットアップして使ってみるとわかるのですが、AppID のログイン画面をカスタマイズ無しで使うとこのような画面になります(後述のものと比較してください)。またカスタマイズ無しの場合、この画面は別ウィンドウで開きます(この挙動も変更できません):
2023031903


ではサンプルを動かしてみましょう。web/.env ファイルだけは実行前に情報を入力しておく必要があります。IBM AppID のインスタンスを作成し、ユーザーを登録し、webapp(ウェブアプリケーション)を登録した結果として得られる Key, Secret, Client_ID, Tenant_ID, Region といった情報を書き写します(SPA_URL はこのままで構いません)。また AppID に登録したアプリの Redirect URI に http://localhost:8080/appid/callback を登録しておいてください:
2023031902


そして2つのアプリを実行します。ターミナルを2つ用意してください。まずは web/ フォルダ側を実行します:
$ cd web
$ npm install
$ npm run start

8080 番ポートでアプリケーションが起動します。そのまま spa/ フォルダ側も(別のターミナルで)実行します:
$ cd spa
$ npm install
$ npm run start

こちらは 3000 番ポートでアプリケーションが実行し、ウェブブラウザが自動で起動します(しない場合は http://localhost:3000 にアクセスします)。以下のような React の画面が表示されれば成功です:

2023031901


この時点ではまだログインできていないので "Login" ボタンが表示されています。このボタンをクリックすると web/ のログイン画面に転送されます(標準の別ウィンドウではなく、同じウィンドウで以下の画面が開きます。上述のカスタマイズ前のログイン画面の UI とも違っていることが確認できます):
2023031902


AppID 標準ではない、カスタマイズされた(日本語の)ログイン画面に遷移しました(ここでは紹介しませんが、オンラインサインアップやパスワード忘れ画面も日本語のカスタマイズ画面を実現しています)。ここで AppID に登録したユーザーの ID とパスワードを入力してログインします:
2023031903


ログインに成功すると元のアドレスに戻ります。今度はログインできているのでログインしたユーザーの情報と "Logout" ボタンが表示されています:
2023031904


Logout ボタンを押すとログイン情報が消え、同じアドレスのまま最初の(ログイン前の)画面に戻ります:
2023031905


フロントエンドフレームワークのアプリで IDaaS を使い、カスタマイズされたログイン画面で認証する、という目的は達成できていると思います。詳しくはソースコード内を参照いただきたいのですが、上述のようなアルゴリズムを実装して、(比較的)セキュアな方法で情報をやり取りしつつ、ログイン画面は標準のものではなく、カスタマイズされた画面を使えています。

注意していただきたいのは、この実現方法は充分にセキュアとは言えない点を持っています。アプリケーション開発者が信用できて、2つのアプリケーション間で情報をやり取りする方法(独自に実装したチャレンジ機能)が「この方法でもよい」と判断された場合のサンプルである、と理解した上で参照していただきたいです。





React.js をはじめとする、いわゆる「フロントエンドフレームワーク」の勉強をしながら色んな機能を実装している中で「あれ?これって・・・」と違和感のように感じた実装内容がありました。自分なりに色々調べたり、ベンダーのサポートに問い合わせたりした結果として「どうやらこれは問題ではないらしい」という結論に落ち着きました。

ただ「これは問題点と分類されることではない」という説明を受けている一方で、「本当に大丈夫なんだろうか?」という違和感は今でもまだ少し残っています。内容が内容だけにどこまで詳しく書くべきかも迷っていましたが、「ベンダーが問題ではないと言っているのだからいいだろう」と割り切って書くことにしました。簡単にいうと「フロントエンドフレームワークを使ったアプリケーションでの認証機能の安全性」についてです。

以下では Auth0 を使ったサンプルを紹介していますが、同様の現象が AWS CognitoIBM AppID でも再現することを確認していることをはじめに記載しておきます(Auth0 を例として選んだのは、この件でのやり取りの中で最も闊達な議論が出来、様々な情報をいただくことができたのでその内容を交えて紹介したいためです。悪意はありません)。細かな実装の違いはありますが、IDaaS に分類される ID 管理のマネージドサービスほぼ全てに共通の事象であると考えていただいていいと思います。


【通常の認証実装と、フロントエンドフレームワークを使った場合の実装との違い】
認証の実装手段による違いを説明するために、以下3種類のアプリケーション例を使うことにします:

A ユーザー管理機能ごと自分で実装し、サーバーサイドでユーザー認証を行うパターン

ある意味で最もシンプルなアーキテクチャです。自分たちで用意するデータベース等でユーザー情報(IDやパスワードハッシュ)も管理する形です。ログイン時はアプリケーションのログイン画面から入力された ID とパスワード(のハッシュ)がデータベース内に格納されているものと一致するかどうかを調べ、一致していればログインを許可する、という流れになります:
a_


これだけなら非常にシンプルとも言えますが、オンラインサインアップやパスワード変更、パスワード忘れといった機能も自分達で用意する必要があったり、既存の他のユーザー情報(社内ユーザーDBなど)との連携時なども自分たちで機能を実装する必要があります。そこまで意識すると必ずしもシンプルな開発が実現できる、とはいえなくなります。


B ユーザー管理機能にマネージドサービス(今回の例では Auth0)を使い、サーバーサイドでユーザー認証を行うパターン

ユーザー管理にマネージドサービスを使うパターンです。Auth0 や AWS Cognito、IBM AppID などが代表的であると思っています。多くの場合で OAuth 2.0 の仕組みが用意されていて、ログイン時のみマネージドサービスのサイトへ飛んでログインし、正しい ID &パスワード情報が入力された場合のみコールバック関数と呼ばれる事前に登録された URL が呼び出される形でアプリケーションの認証が実現できるものです:
b_


上述のAで問題になる、オンラインサインアップやパスワード変更といった面倒な手間も予め用意されているので、ユーザー管理機能まで含めた形で比較的簡単に実装ができるものです。また多くの場合でプログラミング言語ごとの SDK が提供されており、比較的簡単にログイン機能を実装することができます。


C ユーザー認証機能にマネージドサービス(今回の例では Auth0)を使い、クライアントサイドでユーザー認証を行うパターン

Bの仕組みをフロントエンドフレームワーク内で実現するものです。詳しくは後述しますが、フロントエンドフレームワークではコールバック機能が使えないため、PKCE と呼ばれるプロトコルを実装した専用の JavaScript SDK が提供されていることが多いです。今回、自分が問題が発生するのではないかと思ったのはこのパターンです:
c_



ではA、B、Cそれぞれのケースごとに実装やアーキテクチャの違いをセキュリティ面を意識しながら改めて説明します。

まずAのケース。このケースでは利用者が入力した ID とパスワード(ハッシュ)の組み合わせがデータベースのユーザーテーブル内に存在するかどうかを調べることになります。ログイン処理自体はバックエンド側で実装されるケースが多く、フロントエンド側から ID とパスワードを受け取ってパスワードをハッシュ化し、データベースのユーザーテーブル内で同じ ID とパスワードハッシュを持つレコードが存在するかどうかを調べます。存在していた場合は正しい ID とパスワードが入力されたことになるためログインを許可します。この ID とパスワードを受け取った後の一連の処理はバックエンド側で実装される形になるため、利用者からは何をしているのか、どのホストで動いているデータベースにどのような認証情報やプロトコルで接続して、どのような SQL を実行して情報が正しいかどうかを調べたか、といった裏側の情報は一切見ることができません。

一方、悪意を持った利用者がログイン画面から当てずっぽうに ID やパスワードを何度も試す、といった攻撃行為を行った場合でも、そのような記録を(どの IP アドレスから利用しているか、も含めて)アプリケーション・サーバー側のログに残すことができます。したがって「どこまでセキュリティを意識して実装するかにもよるが、比較的安全な実装ができる」と考えることができます。


次にBのケースです。こちらは OAuth 2.0 のコールバックという仕組みを使って実装する想定で説明します。利用者がログインする時に、一時的にプロバイダー(例えば Auth0)が用意するログイン画面に行きます。ここでプロバイダーに登録されたユーザーの ID とパスワード等を入力することが出来た場合にアクセストークンを含めた形で元のアプリケーションの画面に戻ります。アプリケーションではアクセストークンがあるので各種 API を実行できるようになり、ユーザーの詳細情報を取得することもできるようになります。

つまりログイン時に入力される情報は(プロバイダーの画面で入力されるため)アプリケーションからも知ることはできません。またこのログイン画面にリダイレクトする機能や、ログイン成功後のコールバックを受け取る機能については、バックエンド側で ClientID や ClientSecret 、コールバック URL を正しく設定することで可能になります。つまりアプリケーション側でもこの認証部分についてはバックエンド側で処理されることになるため、Aのケースと同様に利用者からは裏側の情報は一切見ることができません

悪意を持った利用者が何度も当てずっぽうにログインを試みる攻撃(ブルートフォースアタック)については、プロバイダー側で「どの IP アドレスから頻繁にログインを試みているか」を特定することができるため、例えばその IP アドレスからのリクエストを遮断する、といった対処が可能になります。


最後のCのケースはどうでしょう?こちらはフロントエンドフレームワークを使っているため、バックエンド側で処理を行うことはできなくなります。そこで PKCE という仕組みを使うのですが、この仕組は充分に安全なのでしょうか? PKCE は悪意のあるアプリケーションからアクセストークンを横取りされないようにするための仕組みでもあるのですが、ではアクセストークンを横取りされなければ大丈夫なのか?という観点で考えてみました。

このCのケースで自分が気になったのは「認証は(アプリケーションから見て外部にある)認証サーバーで」行いつつ、「アプリケーション側にはバックエンドの仕組みがない」という点だと思っています。したがって認証サーバーで認証を行う際に必要な client_id などの情報を安全なバックエンドに持たせることができない、ということになります。

ではどうしているか、というと、実はこれらの情報は全く隠されていません。ビルド時に JavaScript の難読化処理は行われていますが、HTTP でアクセス可能な JavaScript 内に平文で保存されています。この点がフロントエンドフレームワークを使った場合の実装が他のケースと大きく異なる点といえます。


【フロントエンドフレームワークを使った場合に漏れる情報を確認】
実際にこれらの情報が平文で格納されていることを確認してみます。例として実証用に用意したこのサイトを使います:


まず上述のサイトはこちらのサンプルをベースに作っています。このサンプルからはアプリケーション画面の背景色を変えた程度のものを公開しています:


また上述のサイトは Auth0 と、Auth0 が提供するフロントエンドフレームワーク用の SDK を使って実装しています。Auth0 側の設定ではログイン/ログアウトの許可 URL およびコールバック URL は全て同じで、上述の URL そのものを指定しています(これらは本来管理者以外からは見えていないはずのものです)。localhost など、開発/テスト時に指定しがちなもの(=推測される可能性の高いもの)は今回使っていません:
2022050400


このアプリケーションサイトの URL にアクセスすると、以下のようなページが表示されます(元々のサンプルの背景は黒でしたが、後述の比較のため青に変更しています):
2022050401


"LOGIN" と書かれたボタンをクリックすると Auth0 の認証ダイアログが表示され、ここで認証できます。今回はログインしませんが、ログインに成功すると元の画面の "LOGIN" ボタンが "LOGOUT" に変わって、ログアウトできるようになります(ここはサンプルのまま)。今回はそのままブラウザの「戻る」機能で1つ前のページに戻ります:
2022050402


元の画面に戻りました。では改めて公開されている情報をブラウザから確認してみます。画面を右クリックして「ページのソースを表示」を選択します:
2022050403


React によってビルドされた index.html の内容が表示されます。1行で表示されているため少し見にくいのですが、"<script" を検索すると中程にこのような JavaScript をロードしている箇所が見つかります。この JavaScript 部分をクリックして内容を確認します:
2022050404


React と Auth0 SDK によってビルドされた JavaScript ファイルの内容が表示されます。難読化処理が施されているためこれも見にくくなっていますが、この画面で "clientId" という文字列を検索してみます:
2022050405


"clientId" は数か所見つかりますが、その最後の1つの周辺が以下のようになっています:
2022050406

※これ、本当に公開しちゃっていいのか、少し怖いんですけど、後述のように規格としては機密情報ではないらしいので、それを信じて表示しちゃってます。


上図のように "clientId" と "domain" の情報が平文で格納されていることがわかります(わざと clientId が全て見えない位置でスクリーンショットを撮っていますが、実際に試すと全部表示されるはずです)。繰り返しますが、この画面は管理者や開発者だけが見ることのできる画面ではなく、利用者が誰でも見ることのできる画面です。A や B とは異なり、C のケースではこれらの情報をバックエンド側に安全に隠すことができないためです。

ここでは Auth0 を使っている場合の結果を紹介していますが、AWS Cognito や IBM AppID を使った場合でも同様の手順で JavaScript ファイルから設定に使った情報が見えてしまうことを確認しています。


【フロントエンドフレームワークを使った場合に漏れた情報で別のアプリケーションを稼働させる】
さて、上述したようにフロントエンドフレームワークではビルドされた JavaScript から一部の設定内容が見えてしまうことがわかりました。問題はこの状況は安全なのか? です。

ベンダー側の説明をまとめると「これはこのような仕様であり、表示されている情報は機密情報ではない」との認識でした。OAuth 2.0 を定義する RFC6749 でもクライアントを識別する ClientID 自体は単独では機密情報ではない、と明文化されており、これが画面を通じて表示されていることが即イコール危険にさらされている、という認識にはならないと判断しているようでした:
(RFC6749 2.2 より抜粋)The authorization server issues the registered client a client identifier -- a unique string representing the registration information provided by the client. The client identifier is not a secret; it is exposed to the resource owner and MUST NOT be used alone for client authentication

まあ確かにアクセストークンが奪取されるわけではないし、これがわかってもユーザー権限でログインできるわけではない(後述のような環境を作ることができるため、誰にも知られずに好きなだけログインを試すことができる、というだけ)のでギリギリセーフ、といった認識なのだと思います。

とはいえ、それでも全く安心してよいとは思えない点もあります。例えば Auth0 の場合、このフロントエンドフレームワークを使ってアプリケーションを作るために必要な情報は ClientID と Domain 情報、そしてコールバック URL の3つです。前者2つは上述のようにブラウザから確認することができ、また SPA であるためコールバック URL はブラウザのアドレス欄に表示されている文字列(今回の例だと https://dotnsf-auth0.herokuapp.com/)と一致しているはずです。つまり同じ認証機能を使うアプリケーションを作るために必要な3つの情報が全て開示されてしまっていることになります。後は SSL のオレオレ証明書を作って /etc/hosts ファイルを編集すれば、全く同じ Auth0 のサービスを使う、別の(悪意を持った)アプリケーションを作り上げてしまうこともできるわけです:
d_


本当にできるか、実際に試してみました。上述の方法で Auth0 の ClientID, Domain, CallbackURL(今回の場合は https://dotnsf-auth0.herokuapp.com/) の3つを取り出し、これら取り出した情報だけで別のサーバーに別途自分で作ったアプリケーションを適当なサーバーにデプロイして、自己証明書を作って /etc/hosts ファイルも編集した上でアクセスしてみました。自己証明書なので「信頼してアクセス」するための手順は必要ですが、自分でアクセスするだけなので問題なくできてしまいます(オリジナルのアプリケーションとの比較のため、背景を青から赤に変えています):
2022050601


全く同じ認証情報を使って作られているため、Auth0 への認証も(本来のものと同じ条件で)動きます。管理者の知らない所でログインを試すこともできますし、アタックプロテクションが有効になっていれば、アタックがされたことを検知させることもできてしまうはずです:
2022050602


繰り返しますが、上述のアプリケーションでは Auth0 を使ってアプリケーションをビルド&デプロイしていますが、同様のことが AWS Cognito や IBM AppID でも再現します。


【フロントエンドフレームワークを使った場合にこれらの情報が漏れても安全なのか?】
さて問題はこれができてしまうことがどれだけ危ないのか/危なくないのか という判断を行うことになると思います。これを危険だという根拠は以下のようになると思っています:
・本来の管理者の知り得ないところで、自分達が使っているのと同じ認証サービスが使われている
・ブルートフォースアタックが行われていても気付けない
・アタックプロテクションなどの機能が有効になっていると、管理側の知り得ない所からのアタックによってサービスが止まってしまう可能性がある


一方で、規格としてはこれは仕様どおりというか想定通りというか・・・であって、この時点で危険ではないと判断しているようです。以下のような理由であると思います:
・ユーザーのパスワードなどの情報が直接漏れるわけではない
・特定の攻撃者からのみ攻撃できる(一般ユーザーが知らないうちに攻撃に参加している、とか、一般ユーザーが知らないうちに偽サイトに誘導されている、ということは考えにくい)
・アタックプロテクション機能で攻撃を防げる、または攻撃元を特定できる


※ただし特定の条件下でこの C の構成になっている場合、認証サーバー側からも実際のアプリケーションサーバーからも攻撃元を特定できなくなる可能性があると思っています。その具体的な内容はここに書かない方がいいと思ったので控えます。


繰り返しますが、これは OAuth 2.0 の規格としては仕様どおり/想定どおりの挙動であって、(個人情報漏洩といった)直接の被害が起こる内容ではないと思っています。ベンダーとしては規格の上で機密情報とされていない以上は大丈夫、と判断することになるでしょうから、ここまでの情報を元に自分達が管理するサービスにおいて、この機能を使うか使わないか(許可してもよいかよくないか)、を判断すべきことだと思っています。「管理者の知らない所で同じサービスを都合よく使われる事自体が許されない」と判断するのであれば、上述のCのケースのようなフロントエンドフレームワークを使ってのアプリケーション構築を避けて、バックエンド併用の B のような設計にするべき、という意味です。

なんとなく、スッキリとした判断はならないのですが、規格やサービスを提供する側としては「ギリギリセーフ」と判断しているのだと思っています(事実、これが許されないとフロントエンドフレームワークを使ったアプリケーションからの認証サービス利用はできなくなってしまいます)。実際にフロントエンドフレームワークを使って IDaaS の認証サービスを利用するアプリケーションを開発・運用する(可能性がある)場合は、少なくともここで説明したような情報は外部に漏れる可能性がある、という点を理解しておくべきであると思いました。


このページのトップヘ