JWT(JSON Web Token)って何に使うの?仕組みとその利便性


JWTとは

タイトルにもある通り、
JSON Web Tokenの略で
読み方は、「ジョット」です。

なんか音的に、ジェット機を思い浮かべてしまうのは、私だけでしょうか、、、?
なにかものすごく空を飛ぶ印象を受けてしまいます。なので、やりとりが早そうなイメージがまとわりつきますねえ、これは。
ただただ、便利そう◎

JSONの本名が、JavaScript Object Notationなので、

実際は、
JavaScript Object Notation Web Token
ということになるんでしょうかね。

ちなみに、JWTの仕様はRFC7519(外部サイト)で定義されており、
属性情報(Claim:クレーム)をJSONデータ構造で表現したトークンの仕様
※文句のクレームではありませんw

JWTの署名JSON Web Signature(JWS)、暗号化JSON Web Encryption(JWE)にまつわる仕様も、
それぞれRFC7515(外部サイト)RFC7516(外部サイト)に公開されています。

RFCとは

RFC(Request for Comments)は、インターネットで用いられるさまざまな技術の標準化や運用に関する事項など幅広い情報共有を行うために公開される文書シリーズです。
引用元:日本ネットワークインフォーメーションセンター

そもそもトークンとは、ナンゾヤ

トークン (英語: token) ,「しるし」「象徴」、「記念品」「証拠品」の意。
引用:wikipedia

ふむふむ、何かを証明するための印みたいなイメージでしょうかね

英語の類義語としては、
symbol, sign, emblem, mark, badge
などになるので、

つまり、感覚的にいうと、「〇〇証」っていうのがしっくりくるような。

例えば、

  • 許可証
  • 通行証証
  • 身分証

などなど。
つまり、JWTっていうのは、Webアプリケーション上で、「〇〇証」として使う、何か ってことになりますねえ

JWTの構造と仕組み

実際に、JWTの実態をみてみましょう〜
JWTのデバッグができるjwt.ioで生成したJWTそのものを貼り付けてみます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpY2hpX3phbXVyYWkiLCJzdWIiOiJ3aGF0X2lzX2p3dD8iLCJuYW1lIjoiaWNoaV96YW11cmFpIiwiZXhwIjoxNjE0NTI0NDAwLCJpYXQiOjE1MTYyMzkwMjJ9.RscJraSmnAivpcaRiM85Mb8BkdXex1A76SfNjis7igg

ただただ、意味不明な文字列が、ダァぁぁああっと横に伸びていますが、、、
しかも横スクロールしないと見えないし、、、、
これだとなんなのか分からないですね。

以下が、作成した時のスクショです。
こちらは色が付いていて、視覚的に美しい!

何やら3構造になっている。
そして、.で色が区切られているではないかぁああ!!!!

つまり、こういうことか、

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpc3MiOiJpY2hpX3phbXVyYWkiLCJzdWIiOiJ3aGF0X2lzX2p3dD8iLCJuYW1lIjoiaWNoaV96YW11cmFpIiwiZXhwIjoxNjE0NTI0NDAwLCJpYXQiOjE1MTYyMzkwMjJ9
.
RscJraSmnAivpcaRiM85Mb8BkdXex1A76SfNjis7igg
<ヘッダ>.<ペイロード>.<署名>

上述の、横長ベタ貼りのやつは、絶対に.なんて探すの無理。
まじで、無理。マジで、ウォーリーを探せ状態。
常人には気づけなんて、無理ですね、はい。orz

JWTは3構造から成っている

ヘッダー

ヘッダーでは、署名で使うハッシュ値を得るアルゴリズムトークンタイプ(データ型)を指定します。

{
  "alg": "HS256",
  "typ": "JWT"
}

<解説>

alg:署名のハッシュ値を出力するアルゴリズムは、HMAC SHA-256だよ
typ:そのオブジェクトがJWTだよってことを示す

ペイロード

ペイロードでは、実際に取り扱いたい実際のデータを指定します。
クレームデータとも呼ばれ、自分で任意のプロパティを含ませることもできます。
例えば、

{
  "iss": "ichi_zamurai",
  "sub": "what_is_jwt?",
  "name": "ichi_zamurai",
  "admin": true,
  "exp": 1614524400,
  "iat": 1516239022
}

<解説>

name, adminなんかは任意でいれたプロパティ
exp2021-03-01 00:00:00まで有効期限を設定してみました
Unixtime相互変換ツールなんかを使うと便利です。

予約語もあって、それぞれの意味はこちらです。
すべて、指定するかどうかは任意(optional)、つまり自由だぁぁぁぁあああ。

予約語 意味 説明 指定データ型
iss Issuer JWT発行者(サーバー側)の識別子 文字列 / StringOrURI
sub Subject JWTの主語となる主体の識別子 文字列 / StringOrURI
aud Audience JWT を利用する主体(クライアント側)の識別子 文字列/StringOrURI 値の配列
※単一も可能
exp Expiration Time 有効期限日時。期限以降にこのJWTの処理はNG 1970-01-01 00:00:00Zからの秒数を
数値(IntDate)で指定
nbf Not Before 有効期間開始日時。これ以前にこのJWTの処理はNG。 1970-01-01 00:00:00Zからの秒数を
数値(IntDate)で指定
iat Issued At JWTの発行日時。 1970-01-01 00:00:00Zからの秒数を
数値(IntDate)で指定
jti JWT ID JWTのための一意(ユニーク)な識別子。
重複が起きないように割り当てる必要がある。
大文字と小文字を区別する文字列
typ Type コンテンツタイプの宣言 大文字と小文字を区別する文字列

シグニチャー

シグニチャーでは、署名を表します。
翻訳したまんまですね、これじゃまるでわからないのでもう少し解説を。

ヘッダーのオブジェクトを`Base64Url`を使ってエンコード生成した文字列
+
ドット
+
ペイロードのオブジェクトを`Base64Url`を使ってエンコード生成した文字列
,
ソルト(シークレットスパイスを少々)

これをHS256のアルゴリズムで、ハッシュ化して生成されたダイジェスト(文字列)が署名になります。

うーん、なんだかいまいち分からん。

上記の言葉をコード的にまとめると、以下になります。
※以下、なんの言語でもないです、ただのイメージ

data = base64urlEncode( header ) + '.' + base64urlEncode( payload )
signature = Hash( data, secret );

結構簡潔w
つまり、

#ヘッダー
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
#ドット
.
#ペイロード
eyJpc3MiOiJpY2hpX3phbXVyYWkiLCJzdWIiOiJ3aGF0X2lzX2p3dD8iLCJuYW1lIjoiaWNoaV96YW11cmFpIiwiZXhwIjoxNjE0NTI0NDAwLCJpYXQiOjE1MTYyMzkwMjJ9
#秘密キー
hogehogehoge

で生成した署名の文字列が、上記のキャプチャにもあるこれ

RscJraSmnAivpcaRiM85Mb8BkdXex1A76SfNjis7igg

何故、base64urlEncodeを使うのかというと

それは、形式的にいうと、URL-safe(URL的に優しい)にするためです。

JWTは、基本、URIのクエリパラメータとして使用されることを想定していて、
Base64UrlEncodeは、'+'→'-''/'→'_''='→''に変換してくれる

というのも、+ / =予約文字として、確保されてしまっているので、
その文字列がURLに入ってきてしまうと、特別な能力を発揮してしまうため、
無効化する必要があるんですねえ。

つまり、URL-safeにするために、Base64UrlEncodeを使っているということだね!

結局、JWTって何に使うの?

単刀直入に、ユーザー認証に使えます。
何かしらのサービスにログインしたとき、その認証情報をもっておく必要があるが、
その際のユーザー名やその有効期限などを JWT として保持しておくといった感じです。

エンジニア界隈では、
JWTは危ないやら、セッションに使うべきではないやら、
色々と議論が沸き起こっているみたいですが、
確実に言えることは、認証、それです。

認証の方法として、独自でも作れるみたいですが、
OAuth 2.0やら、OpenID Connectやらがあって、
そこでもJWTは活躍してるそうです。
複雑になりすぎので、ここでの説明は割愛します。

JWTの利便性(メリット)

主に、安全性実装のシンプルさ管理の容易さが特徴です

箇条書きで列挙します〜

  • 署名が含まれているため、改ざんがあってもチェックできるようになっていて、一応安全
  • 安全なトークンの発行が比較的簡単に実装できる
  • 認証確認の際に、署名の検証処理だけで済む
  • サーバ側で、ユーザーの認証状態の管理が必要ない(ステートレスに
    • Cookie認証だと、DBにSession情報を保持する必要がある。
  • 認証にDB問い合わせが必要ない
  • ペイロードに任意のデータを詰められてスケーラブル
    • Cookieは4KBで保持できるデータ量が少ない
  • Cookieヘッダを使わない場合に、CORS(Cross-Origin Resource Sharing)の制約も乗り越えられる
  • JSONをデコードすれば、フロントエンドからも直接データにアクセス可能
  • Cookieを使用しないプラットフォーム(モバイルとか)でもJWTが使え、IoTとかの分野でも認証で活躍できるらしい
  • URLのパラメータに含める文字列だから、HTTPリクエストでの取り扱いがシンプル

JWTによる認証の一例

ざっくりとしたJWTを使った認証の流れは、

ヘッダーorペイロードが改ざんされてないかチェック
ペイロードの有効期限(exp)が切れていないかチェック
問題がなければ認証OK

こんな感じですねえ!

ログイン以前

  1. フロントエンドからユーザー情報(ID、パスワード等)をリクエスト
  2. バックエンドで受け取ったIDとパスワードの検証
  3. OKなら、秘密鍵でJWT発行(※認証されたユーザー情報と有効期限を含める)
  4. 発行したJWTをHTTPレスポンスヘッダーのCookieに詰めてフロントエンドに返す
  5. フロントエンドでbase64で変換し、LocalStorageに保存
  6. ログインは完了

ログイン以降

認証されたユーザーがブラウザで、AmazonなどのECサイトで商品購入など、認証が必要な操作をした場合に、

  1. フロントエンドでLocalStorageにトークンがあるか確認
    • ある場合は、その有効期限を確認し、HTTPヘッダに入れてバックエンドにリクエスト。
      • 切れている場合は、バックエンドに問い合わせし、バックエンドでトークンを更新(リフレッシュ)して返す
        • フロントエンドでは、初回登録時と同様に返ってきたトークンをLocalStorageに保存
    • ない場合は、ユーザーの種類によって適切なページへリダイレクト
  2. バックエンドで秘密鍵を使って、受け取ったJWTが改ざんされていないかチェック
  3. バックエンドでJWTの中身を確認して有効期限が切れていないかチェック
  4. バックエンドでOKならそのまま処理を継続、必要に応じてフロントエンドにデータを返す。
  5. バックエンドでNGなら、認証エラーをフロントエンドにレスポンス

JWT認証の注意事項

JWTのヘッダーとペイロードはエンコードされただけのデータなので、
誰でもデコードすればその中身が見えてしまうという事実があります。
盲点ですなあ

なので、改ざんされるリスクがあって、
「でも署名を使ってるから大丈夫だろ〜」って 安心しちゃうじゃないですか?!

そこに落とし穴があって、

{
  "alg": "none",
  "typ": "JWT"
}

"alg": "none"とされてしまったときに、
ライブラリによっては、署名のチェックを素通りしてしまうと言う現象が起こります。

ここが盲点となって、
いろいろあんな恐ろしいことやこんな残酷なことをクラッカーたちは攻撃してきますので、
覚えておきましょう。

ライブラリによっては、"alg": "none"の認証は失敗させるというような処理に対応してきたそうです。
(※具体的にどれがとかは、ちょっとまだ未調査です、すみませぬ。。。)

まとめ

JWTは、便利ですねえええ!!!!!!!!!!!!

認証周りは深いいですねぇ

以上、

ありがとうございました!!