🖱️

【認証】JWTについての説明書

2022/11/19に公開2

はじめに

この記事を読んでいるあなたはJWTについて知っているだろうか?JWTは、認証されたユーザを識別するために最も一般的に使用される。JWTは認証サーバから発行されて、クライアント・サーバで消費される。

今回の記事では、Webアプリケーションの認証方法として最も利用されているJWT認証を簡潔に解説する。

本記事の読者の対象

  • JWT認証について知らない人
  • JWTのメリット・デメリット、仕組みについて詳しく知りたい人
  • アプリケーションの認証方法について詳しく知りたい人

JWTとは

JSON Web Token(JWT)とは、クライアント・サーバの間で情報を共有するために使われる規格の1つである。JWTには、共有が必要な情報を持つJSONオブジェクトが含まれている。さらに、各JWTはJSONのcontentsがクライアントあるいは悪意のあるパーティによって改ざんされないように、暗号(ハッシュ化)を使用して署名されている。

例えば、Googleにサインインする際にGoogleは以下のようなJWTを出力する。

// NOTE: ダミーデータ
{
    "iss": "https://accounts.google.com",
    "azp": "1234987819200.apps.googleusercontent.com",
    "aud": "1234987819200.apps.googleusercontent.com",
    "sub": "10769150350006150715113082367",
    "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
    "email": "jsmith@example.com",
    "email_verified": "true",
    "iat": 1353601026,
    "exp": 1353604926,
    "nonce": "0394852-3190485-2490358",
    "hd": "example.com"
}

上記の情報を用いて、Googleのサインインを利用するクライアントアプリケーションはエンドユーザが誰なのかを正確に把握できるのだ。

Token(トークン)の説明とそれを使う理由

ここで、認証サーバが情報をプレーンなJSONオブジェクトとして送信できないのか、なあぜTokenに変換する必要があるのか疑問に思うかもしれない。万が一、認証サーバがプレーンなJSONとして送信された場合、クライアント側のアプリケーションのAPIは受信しているコンテンツが正しいかどうかを確認する方法がない。攻撃社例えばユーザIDを変更でき、アプリケーションのAPIではそれが発生したことを知る方法がないのだ。

このようなセキュリティ上の問題から、認証サーバはクライアントアプリケーションが検証可能な方法でこの情報を送信する必要がある。ここでToken(トークン)の概念が登場する。

JWTの特徴

JWTの特徴をメリット・デメリットに分けて簡潔に解説する。

メリット

アプリケーションにJWTを使うメリットとして、次のような利点が挙げられる。

  • Tokenベースの認証はスケーラブルで効率的:Tokenはユーザ側で保管する必要があるので、スケーラブル[1]な解決策になる。さらに、サーバはTokenを生成して情報と一緒に検証するだけなので、WEBサイトやアプリケーションでより多くのユーザを一度に維持することが手間なくできる。
  • 柔軟性とパフォーマンス両方に優れる:Token認証は複数のサーバ間で利用でき、多様なWebサイトやアプリケーションの認証を一度に行える。
  • 強固なセキュリティ:JWTのようなTokenは秘密鍵だけがそれを検証できる。

デメリット

JWTは一見完璧な認証方法に思えるが、当然デメリットも存在する。デメリットは以下の通り。

  • 1つのキーだけに依存する:プログラマーが適切に処理しないと、機密情報が危険にさらされる可能性が考慮される。
  • データが冗長になる:JWTのサイズは、通常のTokenのサイズよりも大きい。そのため、Tokenに更に情報を追加するとWebサイトの読み込み速度が遅くなる。
  • 寿命が短い:JWTは有効期間が短いので、ユーザが操作するのがますます難しくなる。これらのTokenには頻繁に再認証を行う必要があり、特にクライアントにとっては煩わしい場合がある。

JWTの仕組み

前提知識

JWTは主に3つの要素で構成されている。

  • header:2つの要素で構成されている―使用されている署名アルゴリズムとTokenの種類
  • payload:JSONデータのcontentsとオブジェクトをふくむ
  • signature:JSONのpayloadの完全性を検証するために使用できる、暗号化アルゴリズムによって生成される文字列
// header
{
    "alg": "HS256",
    "typ": "JWT"
}

// payload(実際には異なる)
{
  "Id": 78912,
  "Quantity": 1,
  "Price": 18.00
}

下記のものがsignatureに該当する。

Base64URLSafe(
    HMACSHA256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ", "NTNv7j0TuYARvmNMmWXo6fKvM4o6nv/aUi9ryX38ZH+L1bkrnD1ObOQ8JAUmHCBq7Iy7otZcyAagBLHVKvvYaIpmMuxmARQ97jUVG16Jkpkp1wXOPsrF9zwew6TpczyHkHgX5EuLg2MeBuiT/qJACs1J0apruOOJCg/gOtkjB4c=")
)

Results in:
3Thp81rDFrKXr3WrY1MyMnNK8kKoZBX9lg-JwFznR-M

payloadの構造

JWTに使われているpayloadのデータは以下の通り。

// NOTE: ダミーデータ
{
    "iss": "https://accounts.google.com",
    "azp": "1234987819200.apps.googleusercontent.com",
    "aud": "1234987819200.apps.googleusercontent.com",
    "sub": "10769150350006150715113082367",
    "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
    "email": "jsmith@example.com",
    "email_verified": "true",
    "iat": 1353601026,
    "exp": 1353604926,
    "nonce": "0394852-3190485-2490358",
    "hd": "example.com"
}
  • iss:トークンの発行者
  • azpaud:Googleがアプリケーションに対して発行したクライアントID。このようにすることで、Googleはどのウェブサイトが自社のサインインサービスを使おうとして売ることを把握し、発行されたJWTの特徴を確認できる
  • sub:ユーザのGoogleID(JWTの主体の識別子)
  • at_hash:アクセストークンのハッシュ。アクセストークンの目的は、クライアントアプリケーションがGoogleに問い合わせて、ログインしているユーザに関する詳細な情報を取得できる
  • email:ユーザのメールアドレス
  • email_verified:ユーザが自分のメールを認証したかどうか
  • iatexp:JWTが作成された時間(エポックからのミリ秒単位)。クライアント・ライブラリがJWTの期限切れを確認する必要がある場合、単にiatフィールドを探せばいい
  • nonce:クライアントアプリケーションがリプレイアタックを防ぐために使用できる
  • hd:ユーザがホスティングしたドメイン

JWTの作り方

  1. 最初に署名タイプと署名アルゴリズムを設定する
  2. headerをBase64で暗号化する
  3. payloadを設定する
  4. payloadをBase64で暗号化する
  5. headerpayload.(ドット)で結合し、署名なしのTokenを生成する
  6. 署名なしTokenに対し、秘密鍵とHMAC-SHA256[2]を用いて署名を生成する
  7. 署名なしTokenと署名を.(ドット)で結合する

JavaScriptで操作する

JWTの生成

Node.jsで実装すると以下のようになる。

create_jwt.js
const crypto = require('crypto')

// base64で暗号化する
const base64 = json => {
    const jsonStr = JSON.stringify(json)
    const jsonB64 = Buffer.from(jsonStr).toString('base64')
    const jsonB64NoPadding = jsonB64.replace(/={1,2}$/, '')
    return jsonB64NoPadding
}

// HMAC-SHA256で署名を生成する
const HMAC_SHA256 = (key, data) => {
    const hash = crypto.createHmac('sha256', key).update(data).digest('base64')
    const hashNoPadding = hash.replace(/={1,2}$/, '')
    return hashNoPadding
}

// headerを設定する
const header = { alg: 'HS256', typ: 'JWT' }

// payloadを設定する
const payload = { sub: '1234567890', iat:1516239022 }

// 秘密鍵の値をsecretという文字列に設定する
const key = 'secret'

// 暗号化したheaderとpayloadを.(ドット)で接続する
const unsignedToken = `${base64(header)}.${base64(payload)}`

const signature = HMAC_SHA256(key, unsignedToken)
const jwt = `${unsignedToken}.${signature}`

console.log(jwt)

上記のプログラムを実行すると以下の通りになる。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
t42p4AHef69Tyyi88U6+p0utZYYrg7mmCGhoAd7Zffs

JWTの検証

test_jwt.js
const crypto = require('crypto')

const HMAC_SHA256 = (key, data) => {
    const hash = crypto.createHmac('sha256', key).update(data).digest('base64')
    const hashNoPadding = hash.replace(/={1,2}$/, '')
    return hashNoPadding
}

const key = 'secret'
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.t42p4AHef69Tyyi88U6+p0utZYYrg7mmCGhoAd7Zffs'

const splits = jwt.split('.')
const unsignedToken = [splits[0], splits[1]].join('.')
const signature = splits[2]

// unsignedTokenは前記のcreate_jwt.jsを参照。
console.log(HMAC_SHA256(key, unsignedToken) === signature)

上記のプログラムを出力すると以下のようになる。

true

おわりに

今回の記事では、アプリケーションの認証として最も一般的に使われている認証であるJWTを簡潔に解説した。これ以上記事が長くなるので、実際のアプリケーションにおける認証でJWTを利用する方法についての解説は後日行う。

参考サイト

https://supertokens.com/blog/what-is-jwt

https://medium.com/system-weakness/hacking-jwt-d29f39e202d5

https://blog.loginradius.com/identity/pros-cons-token-authentication/

https://adsecurity.org/?p=478

https://qiita.com/kaiinui/items/21ec7cc8a1130a1a103a

https://qiita.com/knaot0/items/8427918564400968bd2b

https://zenn.dev/mikakane/articles/tutorial_for_jwt

脚注
  1. スケーラブルとは、機器やソフトウェア、システムなどの拡張性、拡張可能性のことを意味する。言い換えれば、ソフトウェアの場合は小規模な機器から大規模なものまで同じソフトで対応できることを意味する。引用:IT用語辞典 e-words ↩︎

  2. HAMC-SHA256とは、256ビットのハッシュ値を生成できる関数である。 ↩︎

GitHubで編集を提案

Discussion

坦々狸坦々狸

Base64は符号化なので誰でも中身見れるし改竄出来るのでペイロードに機密情報など載せてはいけない事は書いといたほうがいいかも