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


こういったユーザー管理機能を実装しようとした場合、例えばログイン単体機能だけを考えればいい場合はデータベースに 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つのアプリケーション間で情報をやり取りする方法(独自に実装したチャレンジ機能)が「この方法でもよい」と判断された場合のサンプルである、と理解した上で参照していただきたいです。