今回はReact.jsやVue.jsのアプリケーションで利用されるJWT認証の仕組みをやんわり理解していきたいと思います。
JWT(JSON Web Token)とは
JWT(JSON Web Token)は、認証情報(例えばユーザ情報など)を決まったルールによって変換した文字列のことを言います。
具体的には以下の情報で構成されます。
- ヘッダー(Header):トークンのタイプと使用される署名アルゴリズム
- ペイロード(Payload):認証情報などクライアントサイドに共有したい任意の値(再認証時に利用できる情報)
- 署名(Signature):ヘッダーとペイロードとシークレットキーで作成したハッシュ値で、この値を利用することでトークンの改ざん検知を行う。
サーバサイドで作成されたJWTをクライアントサイドが保有していることで、ログイン状態にあることを示してくれる仕組みです。
JWTが活躍するシーン
PHPやRubyのようなサーバサイド言語で作成されたアプリケーションであれば、多くの場合クライアントサイドとサーバサイドが一体となっており、一貫したSESSIONのもとシステム全体で情報の共有が簡単にできるようになっています。
上記のような場合、ログイン認証情報をSESSIONに保管しておけばどこでも認証情報を取得することができます。
JWTは、クライアントサイドとサーバサイドが分離しているパターンのアプリケーションの認証機構として活躍してくれます。
クライアントサイドとサーバサイドが分離している場合、クライアントサイドのみでは認証を行うことができないため、認証を行いたいタイミングでサーバサイドに認証情報を渡して認証をリクエストすることになります。
もちろんリクエストのたびにサーバサイドに認証情報を渡すようにして認証機構を実装することも可能ですが、状況によってはレスポンスが遅くなる原因となるし、なによりサーバサイドへのリクエスト回数が増加し、負荷が心配になります。
そこでこういったケースでは、ログイン画面でのログイン成功時にユーザ情報などから作成したJWTをクライアントサイドに渡し、そのJWTをクライアントサイドのCookieやローカルストレージに有効期限付きで保管します。
クライアントサイドでは、このJWTが保管されている間はログイン状態と判断し、ログイン後のページにアクセスできるよう設計します。
JWTの期限が切れて破棄された後は、ログアウト状態と判断し、再度ログインを促す流れにすればサーバサイドに無駄に負荷をかけることなく認証機構を実装することができるようになります。
JWT発行の仕組み
JWTは最初に説明した通り、ヘッダー、ペイロード、署名の3要素とシークレットキーで作成されます。
// Header
{
'alg': 'HS256',
'typ': 'JWT'
}
// Payload
{
'userName': 'Your Name',
'userId': 'ekdia992Ed',
'email': '〇〇@gmail.com'
}
ヘッダーとペイロードはそれぞれオブジェクトとして用意します。
利用するライブラリによってヘッダーのパラメータが変わる可能性はありますが、大体の記載項目は同じです。
ペイロードの部分は開発者側が好きに設定できます。
ログインしたユーザ情報をそのままペイロードとしてもいいですし、ユーザIDなど再認証時に利用できる最低限の情報でも問題ありません。
(※ JWTは復号可能な文字列なので、外部からの攻撃によってペイロードが漏れる危険性があります。基本的にパスワードなどの漏れたらマズい情報は含めないようにしましょう。)
そしてJWTを構成する3つ目の要素である署名は、このヘッダーとペイロードの値をベースに以下のように作成されます。
これら3つの要素を[①ヘッダーをBase64Urlエンコードした文字列].[②ペイロードをBase64Urlエンコードした文字列].[③署名]という形で組み合わせた文字列がJWTとしてクライアントサイドに返却される値になります。
JWTを使って再認証を行う際は、再認証リクエスト時のJWTの③の値と、①と②の文字列から再度署名を作成したときの値を比較し一致すればJWTの改ざんは受けていないものとして再認証を行います。
もしこの段階で、署名の値が一致しない場合は、①、②、③の値のどれかが改ざんされている可能性があるものとしてJWTは無効であると判断することができます。
JWT認証の実践的な使い方
ここまででJWT認証の仕組みを説明してきたので、なんとなくでもクライアントサイドはログイン時にサーバサイドから受け取ったJWTが存在すればログインしている状態と判断して、JWTが存在しなければログインしていないものとして、ログイン画面に誘導すればいいかなと想像できると思います。
JWTの有効期限を例えば、30分に設定したとしましょう。
そうするとユーザは30分間はログイン後のページにアクセスすることができますが、30分後にログイン状態が解除され、再度ログインページに誘導される形になります。
このとき、ユーザは継続利用していたのにも関わらず操作途中で強制的にログインページに送られてしまい、とんでもなくストレスがかかってしまいます。
ここで問題になってくるのは最初に作成されたJWTを自動更新できないということにあります。
JWTを自動更新できれば、ユーザは再度ログインすることなく継続してシステムを利用することができます。
それを実現するのがアクセストークンとリフレッシュトークンです。
アクセストークンとリフレッシュトークンって何?
と思うのですが、これらは二つとも今まで説明してきたJWTです。
これら二つは同じヘッダー、同じペイロードで作成したJWTなのですが、唯一有効期限に差をつけておきます。
アクセストークンは数十分程度の短すぎず長すぎずの長さでいいと思います。
リフレッシュトークンは、リフレッシュトークンよりも長い有効期限を設けておきます。
このようにアクセストークンがある場合は、ログイン状態と判断し、アクセストークンの有効期限が切れてアクセストークンがなくなった場合は、非同期でAPIコールを行い、リフレッシュトークンを使ってサーバサイドに再認証のリクエストを行います。
リフレッシュトークンが正しければ、新しい有効期限のアクセストークンとリフレッシュトークンを発行し、クライアントサイドに保存します。
こうすることでユーザはシステムを継続的に利用しても、再度ログインすることなく認証情報を更新し続けることができます。
JWT利用の注意点
JWT利用の注意点として、復号可能であるという性質からXSS(クロスサイトスクリプティング)攻撃によってトークンを傍受され、ペイロードの値が漏洩する恐れがあります。
復号にはJWT生成時に利用したシークレットキーが必要なので、単純にクライアントサイドに保存されているJWTのみではペイロードの値を簡単には確認できませんが、万が一シークレットキーもセットで漏れた場合に被害を受ける可能性があります。
そのため、基本的にJWTのペイロードには漏洩して問題のある情報(パスワードなど)は含めないように設計する必要があります。
さらにJWTをCookieに保存する選択をした場合は、httpOnlyを有効にして保存することで、JavaScriptからCookieの値を取得することができなくなるためXSS攻撃への対策を行えます。
その場合は、自分でスクリプトを組んでJWTの内容を取得しようとしても、値を取得できなくなりますので、そのあたりについては何を優先すべきか考えながらアプリケーションを設計していく必要があると思います。
まとめ
というわけで今回はReact.jsやVue.jsの認証機構として利用可能なJWT認証について仕組みや実装の仕方を説明していきました。
きちんと実装方法を設計することで安全に認証機構を提供できますが、クライアントサイドにトークンが保存される仕組み上、攻撃に晒される可能性はゼロとは言えません。
トークンにどの情報を持たせるか、トークンをクライアントサイドのどこに保存すべきかなどXSS攻撃などのリスクを理解した上で実装することをおすすめします。