【認証】JWTについての説明書
はじめに
この記事を読んでいるあなたは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
:トークンの発行者 -
azp
、aud
:Googleがアプリケーションに対して発行したクライアントID。このようにすることで、Googleはどのウェブサイトが自社のサインインサービスを使おうとして売ることを把握し、発行されたJWTの特徴を確認できる -
sub
:ユーザのGoogleID(JWTの主体の識別子) -
at_hash
:アクセストークンのハッシュ。アクセストークンの目的は、クライアントアプリケーションがGoogleに問い合わせて、ログインしているユーザに関する詳細な情報を取得できる -
email
:ユーザのメールアドレス -
email_verified
:ユーザが自分のメールを認証したかどうか -
iat
、exp
:JWTが作成された時間(エポックからのミリ秒単位)。クライアント・ライブラリがJWTの期限切れを確認する必要がある場合、単にiat
フィールドを探せばいい。 -
nonce
:クライアントアプリケーションがリプレイアタックを防ぐために使用できる -
hd
:ユーザがホスティングしたドメイン
JWTの作り方
- 最初に署名タイプと署名アルゴリズムを設定する
-
header
をBase64で暗号化する -
payload
を設定する -
payload
をBase64で暗号化する -
header
とpayload
を.
(ドット)で結合し、署名なしのTokenを生成する - 署名なしTokenに対し、秘密鍵とHMAC-SHA256[2]を用いて署名を生成する
- 署名なしTokenと署名を
.
(ドット)で結合する
JavaScriptで操作する
JWTの生成
Node.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の検証
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を利用する方法についての解説は後日行う。
参考サイト
-
スケーラブルとは、機器やソフトウェア、システムなどの拡張性、拡張可能性のことを意味する。言い換えれば、ソフトウェアの場合は小規模な機器から大規模なものまで同じソフトで対応できることを意味する。引用:IT用語辞典 e-words ↩︎
-
HAMC-SHA256とは、256ビットのハッシュ値を生成できる関数である。 ↩︎
Discussion
Base64は符号化なので誰でも中身見れるし改竄出来るのでペイロードに機密情報など載せてはいけない事は書いといたほうがいいかも
こちらの記事に対する感想を以下のTweetにぶら下げました。
今後JWT関連の記事を書く際の参考にしてもらえたら幸いです。