JWTの基本のキ

はじめまして。もしくはお久しぶりです。 製品開発6年目のはっちゃんです。

製品開発部ではメンバーの技術力向上を目的として、さまざまな取り組みを実施しています。

今回は取り組みの1つである「Tech Discovery」の活動の中でJWTについて調べる機会があったので、その内容をまとめようと思います。

Tech Discoveryは部内でメンバーを選抜し、半期で集中的に技術力向上の取り組みをしていただく企画です。 調査・学習だけでなく実践も通じて、未来のecbeingを支えるアーキテクトの育成を目的としています。

自分で学習した内容をアウトプットとして形にすることで、理解が深まり知識としての定着度も高まります。 今回はTech Discoveryの学習成果としてブログ記事にまとめましたので、是非ご一読ください。

JWT とは

JWT

JWT(Json Web Token:ジョット)は、JSON形式のデータをURLセーフでコンパクトな形にすることで、HTTPヘッダーやクエリパラメータなどで利用しやすくしたデータフォーマット規格になります。 主に認証認可のための仕組みとして用いられることが多いです。

JWTの定義としては、下記になります*1*2

  • JSONデータをBase64でエンコードしたもの
  • JSONで利用するクレーム名は予約語がある

規格にすることで、サービス間のすり合わせが不要になり、定義としてコンパクトなものをデフォルトにすることができるメリットがあります。

JWTにおけるJSONのキーバリューペアは「クレーム(Claim)」とよび、キーは「クレーム名」、バリューは「クレーム値」と呼ばれます。 クレーム名は予約語として定義されているものがあります。 カスタムクレーム名を利用することもできますが、予約語と被らないように注意が必要です。

仕組み

JWTの基本的な仕組みは、「ヘッダ」「ペイロード」の2つの情報を含めたものをそれぞれBase64エンコードし、連結したものになります。

基本的な処理手順としては下記になります。

  1. ヘッダを作成、Base64でエンコードする
  2. ペイロードを作成、Base64でエンコードする
  3. 1,2で生成した文字列を . で連結する

最終的には下記の形になります。

{ Base64Encode(ヘッダー) }.{ Base64Encode(ペイロード) }

上記で生成された文字列をサーバーとやり取りすることで、認証情報をはじめとして様々な情報のやり取りが実現できています。

それでは、ヘッダ、ペイロードについて具体的にどのような内容になっているか見ていきましょう。

予約語

JWTでは、JSONを作るのに利用されるクレーム名は予約されているものがあります。

予約されているクレーム名は下記の表のとおりです。

クレーム名 正式名 説明 利用箇所 必須/任意
typ Type トークンのタイプ ヘッダー 任意
cty Content Type コンテンツタイプ ヘッダー 任意
iss issuer JWTの発行者 ペイロード 任意
sub subject ユーザー識別子などJWTの主体部分 ペイロード 任意
aud audience JWTの受信者 ペイロード 任意
exp expiration time JWTの有効期限 ペイロード 任意
nbf Not Before JWTの有効開始日時 ペイロード 任意
iat issued At JWTの発行日時 ペイロード 任意
jti JWT ID JWTの一意な識別子 ペイロード 任意

JWTでは、これ以外に独自クレーム名を利用することももちろん可能です。

ヘッダ(header)

ヘッダ部分の構造は下記になります。

{
  "typ": "JWT"
}

各クレーム名はそれぞれ下記の役割を持ちます。

  • typ:トークンの種別を表します。通常JWTを指定します

ペイロード(payload)

ペイロード部分の構造は下記になります。

{
  "sub": "1234567890",
  "iat": 1716230000,
  "aud": "<https://api.example.com>",
  "iss": "<https://auth.example.com>",
  "exp": 1716239022,
  "nbf": 1716235000,
  "jti": "e5c2a1f6-d473-4a2c-9b50-2d8bc3b4aef7",
  "name": "John Doe"
}

ペイロードでは様々な情報を詰め込みます。ここがJWTの本体といっても過言ではありません。

  • sub:認証の対象となるユーザの識別子で、通常URI形式で提供されます
  • iat:トークンの発行日時を表します。基本は数値型です。
  • aud:トークンが利用されるべきクライアント(受信者)識別子で、通常URI形式で提供されます
  • iss:トークンの発行者を表す識別子を表します
  • exp:トークンの有効期限を表します。基本は数値型です。
  • nbf:exp とは逆に、トークンが有効となる日時を表します。基本は数値型です。
  • jti:JWT の一意の ID

上記以外にも、独自のクレームを追加することが可能です。 今回は例として name というクレーム名を追加してみました。

JWTの課題

JWTの規格だけでは検証の仕組みが無く、トークンの悪用を防ぐ手段がありません。 そこで署名によって改ざんを検知したリ、暗号化によって読み取りを防ぐといった対応が必要になってきます。

そのため、JWTには署名を付ける、暗号化するなど、いくつかセキュリティ対策を組み込んだ派生形があります。

派生形をまとめて規格にしたものがJOSE(Json Object Signing and Encryption:ジョーズ)で、署名、暗号化、鍵情報の取り扱いについて定義されています。

JOSE

JOSE(Json Object Signing and Encryption:ジョーズ)はJSONを利用したデータ転送用の規格群になります*3

JOSEではJSONオブジェクトの署名、暗号化、鍵格納フォーマットを提供しており、JWTの生成者が署名・暗号化し、受信者が改ざんされていないことを検証するための仕様になります。 署名、暗号化などをJWTと組み合わせたものは、下記で定義されています。

  • JWS(Json Web Signature):整合性保護されたオブジェクト形式
  • JWE(Json Web Encryption):機密保護されたオブジェクト形式
  • JWK(Json Web Keys):キーを表現するためのフォーマット

多くの場合、JWTという文脈はJWS、またはJWEのことを指していることが多いです。

JWS(Json Web Signature)

サーバー側が受け取ったJWTが本物かどうかを確認するための仕組みです。 トークンの署名部分に秘密鍵や公開鍵を使い、データが改ざんされていないことを受信者が確認出来ます。 ヘッダー、ペイロードの改ざん検知は出来ますが、改ざん防止は出来ません。

JWTはHTTPヘッダーで利用することを前提とした表現方法の定義というだけなので、セキュリティについての考慮はほぼありません。

署名を入れることで完全性の検証が可能になります。 事実上ほぼ必須の対応で、JWTという表現はJWSを示すことが多いです。

仕組み

基本的な処理手順としては下記になります。

  1. ヘッダを作成、Base64でエンコードする
  2. ペイロードを作成、Base64でエンコードする
  3. 1,2で生成した文字列を . で連結する
  4. 暗号鍵とヘッダで指定したアルゴリズムを使って、 3で生成した文字列から署名文字列を作成する
  5. 4で生成した文字列をBase64でエンコードし、3の文字列と . で連結する

4,5でJWSの処理が増えています。 JWTを基準に署名文字列を作成し、結合する処理になります。

最終的には下記の形になります。

{ Base64Encode(ヘッダー) }.{ Base64Encode(ペイロード) }.{ Base64Encode(署名) }

末尾にBase64でエンコードされた署名が追加されているのがJWSになります。

それでは、ヘッダ、ペイロード、署名について具体的にどのような内容になっているか見ていきましょう。

予約語

JWSでは、JWT以外に下記の予約語が増えています。

クレーム名 正式名 説明 利用箇所 必須/任意
alg algorithm 署名アルゴリズム ヘッダー 必須
kid Key ID 署名に利用する鍵を識別するためのID ヘッダー 任意
jku JWK Set URL JWKセットが格納されているURL ヘッダー 任意

ヘッダ(header)

ヘッダ部分の構造は下記になります。

{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "12345",
  "jku": "<https://example.com/.well-known/jwks.json>"
}

各クレーム名はそれぞれ下記の役割を持ちます。

  • alg:署名アルゴリズムの種類を表します*4
  • typ:トークンの種別を表します。通常JWTを指定します
  • kid:JWKのキーIDを表します
  • jku:JWKがまとめられているURLを表します

kidjkuについてはセットで、jkuのURLに記載されているJSON形式のキー情報群の中から、kidに合致するキー情報を利用します。 これによって、トークン発行側が秘密鍵でトークンの署名を行い、トークン受信側がjkuに記載されているURLで公開されている公開鍵情報を用いて検証することが出来るようになります。

ペイロード(payload)

ペイロード部分は変化がないので省略します。

署名(verify signature)

署名では、ヘッダーとペイロードの情報を元に署名を作成します。 作成される署名は下記の内容で表現されます。

alg(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret key
)

ここで alg() はヘッダーで指定したアルゴリズムを表しています。

手順としては、まずヘッダーとペイロードで作成したJSONをそれぞれBase64でエンコードし、.で接続します。 それに対して、ヘッダーの alg で指定した署名アルゴリズムを利用して上記の文字列を変換します。 署名の際には、公開鍵を使って処理することが一般的です。

JWSを作る

これまでの情報を元に実際にJWSを作ると、下記のようになります。

ヘッダー

// ヘッダー定義
{
  "alg": "HS256",
  "typ": "JWT"
}// 空白などを取り除く
{"alg":"HS256","typ":"JWT"}// Base64でエンコード
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

ペイロード

// ペイロード定義
{
  "sub": "1234567890",
  "iat": 1716230000,
  "aud": "https://api.example.com",
  "iss": "https://auth.example.com",
  "exp": 1716239022,
  "nbf": 1716235000,
  "jti": "e5c2a1f6-d473-4a2c-9b50-2d8bc3b4aef7",
  "name": "John Doe"
}// 空白などを取り除く
{"sub":"1234567890","iat":1716230000,"aud":"https://api.example.com","iss":"https://auth.example.com","exp":1716239022,"nbf":1716235000,"jti":"e5c2a1f6-d473-4a2c-9b50-2d8bc3b4aef7","name":"John Doe"}// Base64でエンコード
eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzE2MjMwMDAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTcxNjIzOTAyMiwibmJmIjoxNzE2MjM1MDAwLCJqdGkiOiJlNWMyYTFmNi1kNDczLTRhMmMtOWI1MC0yZDhiYzNiNGFlZjciLCJuYW1lIjoiSm9obiBEb2UifQ==

署名

// ヘッダーとペイロードを繋ぐ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzE2MjMwMDAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTcxNjIzOTAyMiwibmJmIjoxNzE2MjM1MDAwLCJqdGkiOiJlNWMyYTFmNi1kNDczLTRhMmMtOWI1MC0yZDhiYzNiNGFlZjciLCJuYW1lIjoiSm9obiBEb2UifQ// secret key:hogehogeを使って、HS256(HMAC + SHA-256)形式の暗号化
alg(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzE2MjMwMDAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTcxNjIzOTAyMiwibmJmIjoxNzE2MjM1MDAwLCJqdGkiOiJlNWMyYTFmNi1kNDczLTRhMmMtOWI1MC0yZDhiYzNiNGFlZjciLCJuYW1lIjoiSm9obiBEb2UifQ, hogehoge)
↓
// 結果をBase64でエンコード
b14isI3c7dlAkqZzyFvCpyY0N785suMvhgo6_HRdN9A

ヘッダー、ペイロード、署名をすべて.で連結

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzE2MjMwMDAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImV4cCI6MTcxNjIzOTAyMiwibmJmIjoxNzE2MjM1MDAwLCJqdGkiOiJlNWMyYTFmNi1kNDczLTRhMmMtOWI1MC0yZDhiYzNiNGFlZjciLCJuYW1lIjoiSm9obiBEb2UifQ.b14isI3c7dlAkqZzyFvCpyY0N785suMvhgo6_HRdN9A

これでJWSを作成することができました。

作成されたJWSはURLセーフになっているので、URLパラメータとして利用することもできます。 基本的には認証認可のためのトークンというユースケースが多いため、HTTPのAuthorizationヘッダーに Bearerスキーマを用いて連携することが多いようです。

下記のサイトで実際にJWSの検証が可能です。 jwt.io

JWE(Json Web Encryption)

データの機密性を確保するための規格です。 データを暗号化し、物理的に読み取りを困難にする対応になります。

暗号化に必要な5つの要素をそれぞれBase64URLでエンコードして、「.」で繋げたものになります。 JWSで防げなかった改ざんの防止が可能になります。

base64UrlEncode(UTF8(JWE Protected Header)) + "." +
base64UrlEncode(JWE Encrypted Key) + "." +
base64UrlEncode(JWE Initialization Vector) + "." +
base64UrlEncode(JWE Ciphertext) + "." +
base64UrlEncode(AWE Authentication Tag)

5つの要素にはそれぞれ下記が設定されます*5

  • Protected Header:暗号化に使用するアルゴリズム
  • Encrypted Key:暗号化に使用した共通鍵コンテンツ暗号化キー(CEK)と言い、暗号化した状態で設定される。要するに別の鍵で暗号化されたコンテンツ暗号化に使われた鍵。
  • Initialization Vector:平文の暗号化に使われる初期化ベクトル
  • Ciphertext:暗号化したデータ本体
  • Authentication Tag:暗号文と追加認証データの整合性を保証する認証タグ。トークン改ざんの有無を検証するために使われる。

Protected Headerでは、キー管理アルゴリズムを指定するalgと、暗号化アルゴリズムを指定するenc(Encryption Algorithm)の指定が必須になります。

イメージとしては、キー管理アルゴリズムによって、暗号化に必要な鍵情報を生成、JWSのような情報をencで指定したアルゴリズムで暗号化し、それらの情報を連携してひとまとめにしたものです。 JWTやJWSの1つ外側の処理のイメージですね。

下記記事のイメージ図がとても参考になりました。

qiita.com

なお、利用する際には暗号化アルゴリズムや手法を吟味する必要があるようです。

JWK(Json Web Keys)

暗号鍵や公開鍵をJSON形式で表現する規格で、JWTやJWEなどで用いられます。

JWKを使うことで、鍵交換や検証プロセスを効率化できます。 特に、認証サーバが公開鍵を配布する際に利用されます。

{
  "kty": "RSA",
  "use": "sig",
  "kid": "1234abc",
  "alg": "RS256",
  "n": "0vx7agoebGcQ...",
  "e": "AQAB"
}

4つの要素にはそれぞれ下記が設定されます*6

  • kty:キータイプパラメータを表し、キーで使用される暗号化アルゴリズムの種類を識別します
  • use:公開鍵の用途を示します。署名(sig)や暗号化(enc)などが指定されます
  • kid:鍵を一意に識別するための識別子です
  • alg:鍵が対応する暗号化アルゴリズムや署名アルゴリズムを表します

これ以外にも、指定するアルゴリズムやキータイプなどによって、いくつかパラメータが変わってきます。 RSA公開鍵を利用する場合は、n(公開鍵モジュラス)やe(公開鍵指数)を表記します。

公開鍵の証明書を利用する場合は、x5ux5cといったパラメータを指定します。

JWKは通常、「JWK Set(JWKS)」として配布されます。

JWKSは複数のJWKを含む配列で構成され、認証サーバがHTTPSエンドポイント(例: https://example.com/.well-known/jwks.json)で提供します。 これにより、受信側は必要な公開鍵を取得して署名や暗号化の検証を行えます。

注意点

実際にプロダクトなどでJWTを利用する場合は、是非ライブラリの利用を検討してください。 仕組みとしては自前実装もできなくはないですが、何かしらの脆弱性を埋め込んでしまってインシデントに繋がりかねません。 JWT.ioでさまざまな言語のライブラリが紹介されています。 是非こちらからの選定をご検討ください。

jwt.io

また、ユーザーのログアウトに合わせてトークンを無効化する処理だったり、トークンの保存先はHttpOnly属性がついたCookieが望ましいなど、通常のWebアプリケーションのセキュリティ同様気を付けることも必要です。 用法容量を守って、正しく使いましょう。

まとめ

今回はTech Discoveryの活動を通して調べたJWTについてまとめました。

木こりのジレンマに陥らないよう、あえて機会を設けて技術的な調査や取り組みを行うことは、斧を研ぐうえで重要だと再認識できました。 今後JWTを使った認証認可を行う際に良いスタートダッシュを切れますし、アウトプットすることで人に教えることもできるようになったと思います。

今後もこういった活動を続けていき、部門全体の技術力向上に貢献したいと思います。

お知らせ

ecbeingでは新進気鋭なエンジニアを募集しております!
careers.ecbeing.tech