はじめに
APIを利用して認証機能をもつサービスを作成しようとしましたが、認証機能でつまづきました。フレームワークやライブラリを利用して実現しようとしていたのですが、そもそも認証やネットワークの根本的な知識が足りないと考え、認証についてまとめることにしました。この記事はあくまで自分の備忘録なので間違っている可能性などもあるのでご了承ください。適宜更新していきます。
認証とは
[1]より引用
通信している相手が本人かどうかを確認する手段
より細かく言うと更に大きく3パターンに分けられ通信相手が正当かどうかを確認する相手認証、改ざんされたかの検出をするメッセージ認証、文書の正当性の保証と本人であることの証明をするディジタル認証の3種類あります。この記事内での認証は一番使われることが多いであろう相手認証のことを指すとします。認証、認可などは誤解を生む言葉であり、いたずらに使うものではないと思いますが、一般的にログイン機能のことを認証と認識してる人が多いのでそういう文脈で使わせていただきます。
通信している相手が本人であるかを判断する材料は通常2つです。
・ユーザーを一意に識別出来る情報
・ユーザーを一意に識別できる情報を持っている本人であることを証明する情報
ユーザーを一意に識別できる情報のことをIDといいます。IDとは(identification)の略でユーザ名、ユーザIDとも言われます。本記事内ではユーザを一意に識別出来る情報のことをユーザIDと呼びます。そしてユーザIDを持っている本人であることを証明する情報のことをパスワードと呼びます。ユーザーIDは当然一意である必要があるので、メールアドレスや電話番号などが使われることが多いです。一意である必要があるので1つのメールアドレスや電話番号で複数のユーザを作成することは出来ません。サービスのユーザ登録の際にメールアドレスが既に使用されていますと出てきたりするのはこのためです。パスワードとして生体情報を使うことも増えてきていますが、本記事では触れません。
利用者の利用範囲が制限されているサービスや機器にIDとパワスワードを入力して利用可能な状態にすることをログインといいます。反対に利用可能な状態から不可能は状態にすることをログアウトといいます。
ネットワークの基礎
認証についてさらに深く掘り下げるためにネットワークの基礎的な部分についても触れておきます。通信の方式として最も一般的なものがHTTPプロトコルです。プロトコルとは通信の際の決まり事であり、そのプロトコルの一種としてHTTPが存在します。送信する側、受信する側が同じ決まり(プロトコル)で通信することを想定して実装してあれば、そのプロトコルを通じてデータのやりとりが可能です。現在コンテンツの送受信に用いられてるプロトコルとしてはHTTPが最も一般的です(本記事ではHTTPSはHTTPの一種だとします)。HTTPプロトコルではWebクライアント(ブラウザ)からWebサーバへのHTMLファイルを要求をHTTPリクエスト、HTTPリクエストを受けたWebサーバがブラウザ側に返すものをHTTPレスポンスと呼びます。
HTTPリクエストは「HTTPリクエスト行」、「HTTPヘッダー」、「メッセージボディ」の3つの部分に分けられます。HTTPリクエスト行にはメソッド、URI、HTTPのバージョンが記述されています。メソッドとは大まかなHTTPリクエストの内容のようなものです。一番よく使われるHTTPメソッドはGETで、このページを見るために皆さんも使っているはずです。URIはURLみたいな情報だと思っておけば問題ないです。HTTPヘッダーにはメッセージボディの補助的なデータが入っています。様々なものがありますが、認証の理解を深めるために必要な知識はクッキーがここに保存されるということです。詳しくはあとで説明しますが覚えておいてください。メッセージボディは何かを入力して送信する場合にここに記述します。ない場合は空です。
HTTPレスポンスは「レスポンス状態行」、「HTTPヘッダー」「メッセージボディ」の3つで構成されています。レスポンス状態行には3桁の数字である状態コード(HTTPステータスコード)が記述されています。見つからない場合に表示される404などは見たことがあるのではないでしょうか。HTTPヘッダーは今回はスルーして、メッセージボディにはHTMLファイルが記述されます。
一連の流れとしては
1.ブラウザからHTTPリクエストを送信(クッキーなどを含む可能性あり)
2.WebサーバがHTTPリクエストを受け取り、解釈し、必要な処理を行う
3.WebサーバがHTTPレスポンスを作成しWebクライアント側に返却
こんな感じです。HTTPメソッドは調べた感じ8個くらいありますがとりあえずCRUDを実現する4つ覚えておけば最低限は大丈夫です。
役割 | メソッド | 意味 |
---|---|---|
Create | POST/PUT | 作成 |
Read | GET | 読み込み |
Update | PUT | 更新 |
Delete | DELETE | 削除 |
HTTPリクエストに対してどのような処理をするかはサーバサイドでプログラミングする必要があります。画面表示のためのリクエストではなく、データなどを取得するためのサーバサイド側のプログラムのことをWeb APIと呼びます(APIの定義はいろいろあるのでこの人はこう考えてるんだなあ程度に考えてください)。フロントエンドがサーバサイド側からデータが欲しい場合はサーバサイド側でAPIを作成し、フロントエンドはサーバーサイドで作られたAPIの形式に従ってプログラミングをする必要があります。
また、HTTPは基本的にはステートレスなプロトコルです。要するにセッション状態を持てないので、セッション状態(ユーザIDなど)を持つためにはブラウザ側のクッキーに保存しておいてHTTPリクエストヘッダにあるクッキーを利用することでステートフルにする手法がよく使われます。しかし、ステートフルであるということは通信量、処理量が増えるということであり、サーバー側の負荷が増えます。一方でECサイトのカートの中身などセッション状態を保持出来ることで便利になる側面もあります。大切なことはサーバーや通信の負荷と、クライアント側の利便性はトレードオフになることです。
ステートレスと関係してRESTという考えがあります。RESTとは設計原則の1つであり、その設計原則の中の一つとしてステートレスであることが挙げられています。RESTの考えを適用したAPIのことをRestful APIまたはRest APIと呼び、APIの設計として採用されることが多いです。しかし、ステートフルなAPIも少なくなく、情報の重要度やサーバーへの負荷などを考慮してAPIの設計をするのが一般的なようです。
セキュリティの基礎
認証の安全性についても掘り下げるためにセキュリティの基礎的な部分についても触れておきます。
クロスサイトスクリプティング(XSS)
XSSは攻撃者の作成したスクリプトを脆弱性のある標的サイトの閲覧者のブラウザで実行させる攻撃のことです[9]。ウェブページのフォーム部分にJavaScriptなどのスクリプトを入力して実行することでクッキーなどの情報が漏洩する可能性があります。クッキーなどに入れる情報は重要なものが多いため、非常に大切かつ基本的な対策しなければならない攻撃だと言えます。一番簡単な対策方法はフォームへの入力に含まれる特殊文字を置換(エスケープ)する方法です。この方法をサニタイジングといいます。
クロスサイトリクエストフォージェリ(CSRF)
この攻撃は名前が長いため「シーサーフ」と呼ばれることが多いです。CSRFは Webアプリケーションにログインした状態で悪意のある罠ページなどを閲覧することで、利用者の意図しないリクエストをウェブアプリケーションに送信することで利用者の権限でアプリケーションを実行する攻撃です。特に金融機関等のアプリケーションなどの場合はお金を扱うので、攻撃者が被害者のお金を攻撃者に送信するスクリプトを用意して踏ませることでお金を盗むことも可能です。一番簡単な対策方法はトークンを利用して正しいリクエストを判断する方法です。トークンとは第三者が知りえない秘密情報であり、リクエストが来るたびにトークンを生成しHTMLフォームにhiddenで埋め込むことでこのトークンとサーバー側が持つトークンが一致する場合のみ通信することで対策出来ます。
SQLインジェクション
SQLインジェクションはアプリケーションのセキュリティ上の不備を利用し、SQL文を実行させることでデータベースシステムを不正に操作する攻撃のことです。ウェブページのフォーム部分にSQL文を直接書くことでデータを取得できてしまいます。一番簡単な対策方法はSQL文の組み立てはすべてプレースホルダで実装することです。SQL文の雛形の中に変数の場所を示す記号(プレースホルダ)をおいて、あとで入力値を割り当てるという方法です。
セッションハイジャック
セッションハイジャックはなんらかの手段でWebアプリケーションセッションIDを入手し、本人に成り代わって通信する攻撃です。セッションハイジャックはセッションIDが推測される場合とセッションIDが盗難される場合があります。従って推測されにくいセッションIDの生成方法にすること、通信が盗聴されないようにsecure属性を加えたクッキーでHTTPS通信を行うことなどがあります。セッションIDはXSSやSQLインジェクションなどでも流出する可能性があるので他の攻撃の対策も必須となります。
クライアント側のデータ保存方法とセキュリティ
認証の際にクライアント側にデータを保存する場合があります。クライアント側でのデータの保存方法としては大きく分けてクッキー(Cookie)、ローカルストレージ(LocalStorage)、セッションストレージ(SessionStorage)、メモリ、OS標準のストレージの5つがあります。
クッキー | ローカルストレージ | セッションストレージ | メモリ | OS標準のストレージ | |
---|---|---|---|---|---|
有効期限 | 任意に設定 | 消さない限り残る | ブラウザが閉じられるまで | スコープが終わるまで | 消さない限り残る? |
容量 | とても少ない | 多い | 多い | 少ない | ? |
サーバーへの送信 | 毎回送信 | しない | しない | しない | しない? |
何が操作するか | サーバーサイド言語かjs(条件あり) | js | js | js | ネイティブ用言語 |
[6]を参考にさせていただきました。
有効期限が大きく違う点、クッキーは毎回HTTPリクエストのヘッダに含まれて送信される点、クッキーはhttpOnly属性をつけることでjsで扱えなくなるのに対し、ほとんどのデータの保存方法はJavaScriptで操作が可能である点がポイントだと思います。
クッキーはWebサーバーごとにWebサーバーがクッキーファイルを作成し、そのWebサーバーに接続する際に専用のクッキーファイルのみを送信します。基本的には他のクッキーファイルにはアクセス出来ませんが、過去には他のクッキーファイルにアクセスできてしまうブラウザ側のセキュリティホールが見つかったこともあります[7]。しかし、現在の状況では重要な情報は適切な期限を設定したクッキーに入れることが多いのではないかと思います。理由としては重要な情報をある程度長い期間クライアント側で保存する方法としてクッキーとローカルストレージがありますが、クッキーではhttpOnly属性を設定することでJavaScriptで扱えなくなるので安全であるというのが一般的であるからです。しかし、この主張自体にもかなり議論があり、httpOnlyをすれば完全に安全になるわけではないこと、Javascriptでクッキーを扱う必要がある場合はhttpOnlyには出来ないことから**「重要な情報はクッキーに保存すれば絶対安全!」になるわけではない**ようです。また、Secure属性をつけることでHTTPSでしか送信されないため、通信の盗聴に多少強くなります。可能ならクッキーにはhttpOnly属性とSecure属性をつけて利用するべきでしょう。
ローカルストレージは第2のクライアント側の長期保存の方法となっていますが、先程述べた理由から「クッキーの方がいいよね」という風潮になっている印象です。もともとローカルストレージは複雑なSPAでstringのキーを保存するために設計されたものであるそうで、セキュアな設計ではないそうです[14]。JavaScriptでクッキー扱う必要が全くない場合は基本httpOnlyなクッキーでいいかなと思いますが、JavaScriptによるクッキーの操作が必要な場合(あんま具体例が思いつきませんが)はローカルストレージも選択肢に入ってくるかなという感じです。クッキーとローカルストレージの違いとしてサーバーへ送信されるかどうか、容量、有効期限、CSRFの危険性が挙げられます。クッキーは毎回送信されるのにたいし、ローカルストレージは必要な場合のみ送信するので通信量の削減になります。しかし、これは実現したいものの仕様や通信するもののサイズに依存するため無条件で利点になるとは言い難いです。ただ、クッキーで送信するとCSRFの危険性がありますが、ローカルストレージは自動的に送信されないのでCSRFの危険性がそもそもありません。しかし、XSSの対策が困難であることに対して、CSRFの対策は容易であることから、これもたいしたメリットにはならないかなと思います[16]。容量はクッキーよりも多いですが、そもそもそんなに大きなサイズのものをクライアント側に保存して通信しなければいけない仕様に疑問を感じます。有効期限に関してはローカルストレージは手動で消さない限り残ることを考えると、クッキーの方が安全性が高いように感じます。ここまでの議論を見てみるとローカルストレージを使う場面というのは、JavaScriptでクッキーを扱う必要があり、サイズの大きなものを保存しておく必要があるという限定的な場面ということになるかなと思います。ただ先程も述べましたが、httpOnlyだから安全であるという主張については議論されているので、もしかしたらローカルストレージの方が良いみたいな流れになる可能性はあります。詳しく知りたい人は「CORS クッキー ローカルストレージ」で調べればいろいろ出てくると思います。また、ローカルストレージは比較的新しいのでローカルストレージが使えるかどうかをきちんと確認する必要があります(調べるの面倒なので割愛)。
セッションストレージはページをまたいで情報を渡したい時に使うものです。ブラウザを閉じたら無くなるので、長期的な保存にはもちろん向いていません。phpの$_SESSIONとは別物ですので注意してください。
JavaScriptのメモリは変数に代入してスコープが終わったら自動的に破棄されます。JavaScriptを使う場合はこれが最強だと思いますが、保存されてないのでページから離れるとだめなのでソフトウェア要件次第というところです。まあ大体のソフトウェアは無理そうな気はします。
OS標準のストレージについては僕も調べていて初めて知ったのですが、モバイル系のOSは標準で提供しているストレージが存在し、そこに保存することもあるそうです[15]。例としてiOSのKeyChainやAndroidのKeyStoreなどがあるそうですが、調べても全然分からなかったのと、ネイティブアプリに関しては完全素人なのでこんなものがあるらしい程度の紹介にとどめておきます。
データの保存方法とセキュリティ的な観点から考察をしていきましたが、結局どの方法も一長一短であり、銀の弾丸のようなものはないようです。状況に応じてデメリットをなんとか最小限に抑えて仕方なく使っているような印象を受けます。JavaScriptでクッキーを扱わないならhttpOnlyなクッキー、JavaScriptでクッキーを扱う場合で大きいサイズのものを保存したい場合はローカルストレージに保存するというのが個人的な結論かなと思います。まあ一番の解決策は保存しないことだと思うんですが、保存しないことにより生じる不便さをユーザーは許してくれないように思います。それなのにデータが取られたら非難されそうです。
認証方式
Webサービスの認証について調べたところ、セッション認証とトークン認証の2つの認証をよく見かけるので、この2つに絞って紹介しようと思います。
セッション認証
セッションとはアクセスの開始から終了までの一連の通信のことであり、セッションを用いない場合HTTPリクエストをする度にユーザIDとパスワードの入力が必要になります。セッション認証では最初にクライアントからサーバーに認証情報を送信し、認証に成功した場合はセッションを開始し、セッションIDを生成します。サーバーからのHTTPレスポンスヘッダのSet-CokkieによってブラウザのクッキーにセッションIDを保存します。クッキーが有効な間はセッションIDがクッキーに保存されているのでログイン状態が維持されます。基本的にセッション認証ではクッキーを用いることが一般的です。
トークン認証
トークン認証ではセッションIDの代わりにトークンと呼ばれるものを用います。トークン認証では最初にクライアントからサーバーに認証情報を送信し、認証に成功した場合はトークンを返します。このトークンを保存し、HTTPリクエストヘッダに含ませてHTTP通信をすることでサーバー側のトークンと一致した場合は認証に成功します。トークンをどのように発行するかやトークンの保存場所については後述します。
その他の選択肢としては自分で実装することを諦め、Auth0などのIDaasを採用するなどもありますが、ユーザー数が増えてからの料金が多いことやIDaasから違うものに変更する際に大変などの問題点があります。それにIDaasを使うにしても自前である程度の認証機能は作れないとセキュリティ的にまずいと思うので、今回は自前で実装する場合を想定して話を進めていきます。それでは、それぞれの認証方式について深く掘り下げていきたいと思います。
セッション認証
基本的にセッション認証はRuby on RailsやLaravelなどのフルスタックフレームワークなどで使われることが多く、SPAの認証ではあまり使われていないようです。理由としてはSPAにする場合はフロントエンドとバックエンドを完全に疎結合にすることでWeb、ネイティブアプリ、デスクトップアプリなどで同じAPIを使えたり、コードが見やすくなり開発が簡単にするメリットを得たいためだと思います。また、API設計としてRESTを採用した場合、セッションはステートフルなため設計原則からずれた仕様のAPIを作成せざるを得ません。そもそもクッキーはページ間の状態保持のためのものですが、真のSPAなら1ページなのでJavaScriptの変数に入れてしまえばいいです[25]。このような背景からセッション認証はSPAではあまり採用されず、フルスタックフレームワークでの採用が多いです。httpOnly属性とSecure属性を利用したクッキー使用のセッション認証はある程度の安全性を簡単に提供できます。しかし、REST APIを用いた疎結合なSPAアプリケーションはそれを上回る開発効率とシンプルさがあるということだと思います。ただ、安全性が大切なSPAアプリケーションではセッション認証も使われているので状況次第です。
トークン認証
上述したようにSPAの認証でよく用いられます。トークン認証ではOAuthというアクセストークンを発行する仕組みがよく使われます。OAuth2.0を拡張してアクセストークンの発行だけでなく認証周りの決まりを標準化したものがOpenId Connectです。他のサービスのアカウントを使う際にOpenId Connectを使うことが多いです(厳密には違うようですが[19])。OpenId Connectではアプリケーションから他サービス(Google、Twitterなど)にユーザー情報が含まれたアクセストークンであるIDトークンというものを要求します。他サービスはユーザーにこのサービスにIDトークンを発行するか聞きます。ユーザーが許可した場合、他サービスはIDトークンをアプリケーションにIDトークンを発行します。このIDトークンの形式として**JWT(ジョット)**というものが使われています。(正しくは署名付きJWTなのでJWSだがみんなJWTを使う)
順番 | 名前 | 役割 |
---|---|---|
1 | ヘッダー | 署名の検証に必要な情報 |
2 | ペイロード | ユーザー情報などデータ本体 |
3 | 署名 | 検証する内容 |
[19]を参考にさせていただきました。
JWTは上の表のような構造になっており、それぞれBase64というものでエンコードされた文字列になっています。注意しなければならないのはBase64というのは暗号ではなくASCIIのような文字コードのようなものです(Base64では画像なども変換出来るようです[20])。つまりこれは簡単に変換できるものであり、JWTが盗まれるということはユーザー情報を盗まれるということと同義です(Base64変換ツール[21])。また、ユーザー情報そのものを含むためデータ量が比較的大きくなりがちという特徴があります。JWTの署名には秘密鍵を使うため、これを用いて検証することでパスワードなどの認証情報をデータベースに保存する必要がありません。それらの情報はすでにJWTに含まれているためです。
このJWTをフロントエンドとバックエンドでやりとりするためにはHTTPリクエストヘッダーに含めるしかありません。JWTを入れてやりとりするのに適切なHTTPリクエストヘッダーとしてCookieヘッダーとAuthorizationヘッダーがあります。2つのヘッダーの大きな違いはフロントエンド側での操作が必要かどうかと毎回送信されるかです。Cookieヘッダーはクッキーを入れるところであり、サーバーからのHTTPレスポンスにあるSet-Cookieヘッダーによってブラウザにクッキーを保存します。Cokkieヘッダーに含まれるクッキーは対象のWebサーバーに毎回送信されることになります。Authorizationヘッダーの場合、APIによって取得したJWT形式のトークンをフロントエンド側のJavaScriptで操作する必要があります。しかし、クッキーに保存していないなら必要なときのみ送信するため、CSRFの危険性はありません。
つまり、CookieヘッダーかAuthorizationヘッダーを使うかという問題はJWT形式のトークンをクッキーに保存するかローカルストレージに保存するかという問題に非常に近いです。Cookieヘッダーを使えばクッキーをhttpOnly属性とSecure属性にすることでXSSなどの危険性を減らすことが出来ますが、ステートフルなAPIになってしまい、CSRF対策のために別にトークンを作成する必要もあります。反対にAutorizationヘッダーを使う場合、APIによって取得したトークンをJavaScriptによってセットする必要があります。クッキーに保存する場合httpOnly属性にすることが出来ず、クッキーの利点が薄れます。ローカルストレージに保存すればCSRF対策の必要もなく、毎回通信しないので通信量が減る可能性があり、ある程度大きなサイズのものも扱え、RestfulなAPIを作成できます。トークンどっちに保存するか問題は多分ここらへんについて議論しているのだと思います。
OpenId Connectは絶対に使わないといけないものではなく、OAuth2.0とJWTで使っても良いでしょう。ここらへんに基礎的な部分を抑えた上でどのような認証にしたいかを考えていく必要があると思います。
ここからは今まで調べたことをもとによく議論されていることについて考えていきたいと思います。
順番 | メリット | デメリット |
---|---|---|
LocalStrage | CSRF対策いらない 容量大きい APIがステートレス 毎回送信しない |
XSSの危険性が比較的高い 対応していない可能性 |
Cookie | XSS耐性比較的高い フロントエンド側で操作不要 |
毎回送信する CSRF対策が必要 APIがステートフル |
正直どっちが良いとかの議論をする意味がないと思います。ぶっちゃけどっちも安全じゃないです。大切なのはメリット、デメリットを理解して使うことかなと思います。ただ高いレベルのセキュリティでトークン認証を使いたい場合はクッキーに保存してCSRFトークンも使うのが良いかなと個人的には思います。
あと2択みたいになってることが多いですが、メモリへの保存も考えてみた方が良いと思います。一番安全なのは長期的に保存しないことです。
おわりに
認証まわりはやはり複雑ですね。今回の記事で少しは理解が深まったかなと思っています。誰かの役に立てば嬉しいです。
#参考文献
[1]:認証の仕組みと必要性
[2]:HTTPとは
[3]:第2回 HTTPプロトコルの詳細
[4]:超絶初心者のためのサーバとクライアントの話
[5]:HTTPリクエスト/レスポンスとは? HTTPヘッダーを理解しよう
[6]:CookieとWebStorageとSessionについてのまとめ
[7]:Cookieの仕組み
[8]:0からREST APIについて調べてみた
[9]:クロスサイトスクリプティング
[10]:知っておきたいクロスサイトリクエストフォージェリの仕組み
[11]:安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ)
[12]:安全なウェブサイトの作り方 - 1.1 SQLインジェクション
[13]:安全なウェブサイトの作り方 - 1.4 セッション管理の不備
[14]:HTML5のLocal Storageを使ってはいけない(翻訳)
[15]:ユーザー認証とは。Web、APIでの認証の仕組みと認証方法
[16]:JWTなどのTokenをlocalstrage(HTML5の)に保管することについて
[17]:(徳丸 浩さんのwebサイト)
[18]:SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜
[19]:ユーザー認証とは。Web、APIでの認証の仕組みと認証方法
[20]:base64ってなんぞ??理解のために実装してみた
[21]:base64エンコード/デコードツール
[22]:【REST API】認証トークンをどのように扱うのが良さげか調べたことをまとめる
[23]:一番分かりやすい OAuth の説明
[24]:JWT認証と流れのやわらかい解説
[25]:SPAでのログイン、認証について