- ■はじめに
- ■認証とは?
- ■JWT認証とは
- ■セッション認証方式
- ■JWTの構造
- ■HMAC(Hash-based Message Authentication Code)とは
- ■アクセストークンとリフレッシュトークン
- ■JWTを使った認証フローを確認してみる
- ■悪者がJWTを盗んで偽造した場合に検知される仕組み
- ■まとめ
- ■参考記事
■はじめに
株式会社iimonでエンジニアをしている「白水(しらみず)」です。
みなさま、明けましておめでとうございます。
本年もよろしくお願いいたします。
新規サービスの開発にあたり認証周りをどうするか考えていたのですが、その中でJWT認証について調べる機会があったので、まとめてみました。
図も交えて解説しているので、ぜひご覧ください。
■認証とは?
認証とは、「本人確認してこの人は本人だね、使ってOKと判断する仕組み」のことです。
要するに、ログインなどは「認証」という分類になります。
似たような言葉に「認可」がありますが、「認可」は許可をするかどうかという意味です。
例えば、Aさんは「AWSのSecretManagerの値を見れる」けど、Bさんは見れないというように、個人や個人を集めたグループに対して、何を許可して、何を許可しないかを与えることを「認可」といいます。
■JWT認証とは
JWT認証の特徴としては、以下があります。
- JWTは、JSON Web Token の略である
- トークン自体にユーザーを識別する情報が含まれている
- サーバーはDBを参照せずにトークンが正しいか検証できる
- 署名(Signature)により改ざんを検知できる
特に、私は③と④がピンと来ないなーと感じていました。
■セッション認証方式
◆処理フロー

- クライアントがログインする前にページにアクセスすると「CSRFToken」をサーバーから受け取って、ブラウザ内のCookieに保存してページを表示します。
クライアントがログインするときに、ユーザー名とパスワードをサーバーに送りますが、その際にCookieに保存されている「CSRFToken」もサーバーに送信します。(ブラウザが勝手に送ってくれます)
サーバー側では、ユーザー名とパスワードで認証をして、認証がOKであれば「セッションID」を作成して、DB または RedisなどのNoSQLDBに保存します。
このときに、サーバーが発行した「セッションID」と「ユーザー情報」が紐づいた形で保存されます。
サーバからクライアントへのレスポンスでステータスコードで「200番」を返しますが、レスポンスヘッダーの中に「Set-Cookie」というキーと「サーバーが発行したセッションID」をkey: valueの形でクライアントへ返します。
クライアントは、ブラウザ内のCookieに「サーバーが発行したセッションID」を保存します。クライアントから他のページにアクセスするときに、ブラウザ内のCookieに保存している「セッションID」をサーバーに送信します。
サーバーは、クライアントから送られてきた「セッションID」と同じ値がDB または RedisなどのNoSQLDBに保存されているか確認して、保存されていれば認証済みとして、何かしらのデータをクライアントに返します。
ログアウトする場合は、DB または RedisなどのNoSQLDBに保存している「セッションID」を削除することで可能にします。
このあたりは、ステートレスとステートフルという知識も必要なので、以下記事なども確認ください。
◆セッション認証方式の課題点
セッション認証には、以下のような課題があると思っています。
- 各リクエストでDB/Redis へのアクセスが必要
- サーバー側でセッション状態を管理する必要がある
- サーバーAとサーバーBがあって、サーバーAにはユーザー①の「セッションID」があるけど、サーバーBにはユーザー①の「セッションID」がないので、ユーザー①がサーバーBにリクエストを送ると弾かれる。
なので、サーバーAとサーバーBで共通の「セッションID管理」が必要になる
しかし、セッション認証は、現在でも広く使われていると思います。
特に、サーバーサイドでHTMLを組み立てるようなサイトの場合は、セッション認証が使われている気がします。
逆に、SPAサイト系はJWT認証が好まれているのかなと思っています。
■JWTの構造
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0NX0.XXXXXXXXXXXXXXXX |______ Header ____|.|______ Payload ______|.|__ Signature __|
JWTを実現するAccessTokenは、3つの部分に分けられます。
Header(eyJhbGciOiJIUzI1NiJ9)
Payload(eyJ1c2VyX2lkIjoxMjM0NX0)
Signature(XXXXXXXXXXXXXXXX)
// Base64 エンコード btoa('{"user_id": 123}') →'eyJ1c2VyX2lkIjogMTIzfQ==' // Base64 デコード atob('eyJ1c2VyX2lkIjogMTIzfQ==') → '{"user_id": 123}'
Base64 エンコードは、JSON → Base64文字列 に変換することです。
Base64は誰でも簡単にデコードして中身を見れるので、Payloadに機密情報(パスワード等)を入れてはいけません。
Header(ヘッダー)
{
"alg": "HS256", ← 署名アルゴリズム(HMAC-SHA256)
"typ": "JWT" ← これはJWTですよ
}
Payload(ペイロード)
{
"user_id": 12345, ← 誰か
"exp": 1705312800 ← AccessTokenの有効期限(Unix時間)
}
Signature(署名)の作り方
HMAC-SHA256(Header + "." + Payload + SECRET_KEY)
※ SECRET_KEYを知っている人だけが正しい署名を作れる
※ 内容を1文字でも変えると、署名が合わなくなる
JWTのAccessTokenを作る3つの構成は、上のようになります。
Headerには「署名アルゴリズム」と「これはJWTですよ」というtypeがオブジェクトの形式で入っています。
Payloadには、ユーザーを判別するためのidなどの情報と、AccessTokenの有効期限がオブジェクトの形式で入っています。
Signatureは署名をするための印鑑のような役割で、「Header + "." + Payload + SECRET_KEY」を組み合わせた値をHMAC-SHA256(Hash-based Message Authentication Code)という暗号化の関数を通して暗号化することで作成します。
SECRET_KEYは、外部に絶対に知られてはいけない情報なので、AWS SecretManagerなどで本番では管理して、それを使うようにします。
■HMAC(Hash-based Message Authentication Code)とは
「メッセージ認証コード」を作るための仕組みです。
メッセージ(データ)+ 秘密の鍵を組み合わせることで、固定長の文字列を作成します。
JWTでは、以下のような形になります。
- メッセージ(データ):Header + "." + Payload
- 秘密の鍵:SECRET_KEY(AWS SecretManagerなどに持つ)
- 出力: 署名(Signature)が発行される
Signatureの作成の流れは、以下のようになります。
SECRET_KEY = "xK9#mP2$vL5nQ8..."(サーバーだけが知っている情報)
【Aさん(user_id: 123)の署名】
入力 = "eyJhbGci..." + "." + "eyJ1c2VyX2lkIjoxMjN9"
鍵 = "xK9#mP2$vL5nQ8..."
↓
HMAC-SHA256
↓
出力 = "ABC123XYZ" ← Aさん用の署名
【Bさん(user_id: 456)の署名】
入力 = "eyJhbGci..." + "." + "eyJ1c2VyX2lkIjo0NTZ9"
鍵 = "xK9#mP2$vL5nQ8..." ← 同じSECRET_KEY
↓
HMAC-SHA256
↓
出力 = "DEF456UVW" ← Bさん用の署名(Aさんとは違う値)
特徴としては、以下があります。
- 同じ入力 → 必ず同じ出力が得られます。
- SECRET_KEYは全ユーザー共通です。
- Payloadが違うと、署名(Signature)は異なります。
- 出力から入力を逆算できない特徴があります。(SECRET_KEYが外部に漏洩したら、ワンちゃんわかるかも)
- 入力が1文字でも変わると、出力が全く違う値になります。
- 秘密の鍵を知らないと、正しい出力を作れないです。
- SECRET_KEYは全ユーザー共通(1つだけ)ですが、Payload(例:user_id など)がユーザーによって異なる違うため、署名も違う値になります。
- DBに「セッションID」のように保存しなくても、認証の仕組みが成立します。
■アクセストークンとリフレッシュトークン
JWTには、アクセストークンとリフレッシュトークンという2つのトークンがあります。
なぜ2つあるかと言うと、アクセストークンに30日間くらいの長い有効期限を設けていると仮定して、アクセストークンを悪い人に盗まれたとします。
JWTでは、DBにアクセストークンを持っていないので、30日間は悪い人に悪用されることになります。
そこでアクセストークンには「15分〜1時間」くらいの短い有効期限を設けて、リフレッシュトークンに対して、「30日くらいの長い有効期限」を設けて、リフレッシュトークンをDBに保存しておきます。(リフレッシュトークンは、DBに保存しないで運用するサービスもあるので、あくまで一例とします)
そうすると、悪い人にアクセストークンを盗まれたら、DBのリフレッシュトークンを削除することでアクセストークンには「15分〜1時間」くらいの短い有効期限を設けているので、1時間くらいで無効になって、悪用を最低限の抑えることができます。
まとめると、以下のような役割に分けられます。
- アクセストークン
- 用途:API呼び出し
- 有効期限:短い(1時間と仮定する)
- 保存:DB保存しない
- 盗まれたら:1時間で無効
- リフレッシュトークン
- 用途:アクセストークン再発行
- 有効期限:長い(7日〜30日)
- 保存:DBに保存する(保存しなくてもJWT認証は実現できる)
- 盗まれたら:DBから消せばOK
アクセストークンとリフレッシュトークンのライフサイクルは以下のようになります。

■JWTを使った認証フローを確認してみる
ログイン(トークン発行)

ログインの流れは、以下になります。
- ログインフォームにメールアドレスとパスワードを入れて、サーバーに送る
- サーバー側でDBからメールアドレスが同じユーザーを探す
- ユーザーが存在すれば、ユーザーが送ってきたパスワードをhash化して、メールアドレスが同じユーザーのハッシュ化されたパスワードが同じかどうか検証する。(hash化したパスワード同士で検証する)
- パスワードが一致したら、AWS SecretManagerからJWT用のSECRET_KEYを取得する
Header+"."+Payload+HMAC(Header + "." + Payload + SECRET_KEY)+“有効期限(1時間)”でアクセストークンを作成する- refreshToken(7日)を作成して、DBに保存する。
- アクセストークンをユーザーに返す
- ブラウザのlocalStorageかCookieに保存する。
┌──────────┬─────────────────┬──────────────┬─────────────┬─────────────┐ │ id │ email │ password_hash│refresh_token│refresh_exp │ ├──────────┼─────────────────┼──────────────┼─────────────┼─────────────┤ │ uuid-123 │ tanaka@test.com │ $2b$12$... │ xyz789... │ 2025-01-21 │ │ uuid-456 │ suzuki@test.com │ $2b$12$... │ NULL │ NULL │ └──────────┴─────────────────┴──────────────┴─────────────┴─────────────┘
ログインした時点でのusersテーブルの状況としては、上のようになります。
ログインした人だけトークンが入っています。
access_token は DB に保存しないで、refresh_token は DB に保存している状況になります。
処理フローの中でも、⑤のHMAC(Header + "." + Payload + SECRET_KEY)部分で、SignatureはHeader + "." + Payload + SECRET_KEYで作られていることを理解しておいたほうが、後々のアクセストークンが盗まれた場合の話で理解しやすいと思います。
API呼び出し(アクセストークン検証)

API呼び出しの流れは、以下になります。
- フロントで保存していたアクセストークンを添付してAPIを叩く
- AWS SecretManagerからJWT用のSECRET_KEYを取得する。
- フロントから送られてきたアクセストークンを「.」で区切って、
HeaderとPayloadとSignatureに分解する。 - フロントから送られてきたアクセストークンの
Header + "." + PayloadとSECRET_KEYをHMAC()に通して、アクセストークンを再計算する。
サーバー側で再計算したアクセストークンと、フロントから送られてきたアクセストークンが同じであれば、改ざんされていないので次に進む。 - アクセストークンの有効期限を確認する。
- 顧客データをDBから検索して、フロントに返す。
アクセストークンを使ったAPI呼び出しでは、以下の特徴があります。
- 認証に関して、DBへのアクセス不要(セッション認証はDBアクセスする)
- 署名検証のみで認証完了
- 改ざん検知が可能
- JWTは「無効化できない」というデメリットもある
特に④のアクセストークンを再計算する部分は、「フロントから送られてきたアクセストークンのHeader + "." + Payload と SECRET_KEYをHMAC()に通して、アクセストークンを再計算する。」という部分が大事で、これにより改ざん検知できます。
改ざん検知は、後半で解説します。
トークン更新(リフレッシュ)

トークン更新の流れは、以下になります。
- クライアントから商品一覧ページのAPIを叩く(アクセストークンをサーバーに送る)
- サーバー側でアクセストークンの有効期限を確認すると、切れているので「401 Unauthorized」 エラーを返す
- クライアントからリフレッシュトークンのAPIを叩く(ローカルストレージかCookieに保存しているリフレッシュトークンも一緒にサーバーに送る)
- サーバーでDBからリフレッシュトークンが同じレコードを探す
- リフレッシュトークンの有効期限を確認する
- 新しいアクセストークンを発行して、有効期限を伸ばす
- クライアントにアクセストークンを返す
- クライアントはアクセストークンをローカルストレージかCookieに保存して、もう一度商品一覧ページのAPIを叩く
このフローの中の特徴として
- リフレッシュトークンはDBで管理するため、ログアウト時に無効化できる
- リフレッシュトークンのAPIを叩いたときだけ、DBアクセスが発生する(リフレッシュトークンをDBに保存しないパターンでは、DBアクセスは発生しないので例外あり)
- localStorage保存は避ける(XSS対策)方が良いらしい。Cookie保存が一般的?には推奨らしいです。
ログアウト

ログアウトの流れは、以下になります。
- クライアントはログアウトのAPIを叩く
- アクセストークンの中に含まれるユーザーの識別情報から、DBにアクセスして、リフレッシュトークンを削除する
- クライアントにログアウトしたメッセージとステータスコードを返す
このフローの中の特徴として
リフレッシュトークンは、DBから消したので即無効になります。
そのため、削除以降は新しいアクセストークンは発行できません。アクセストークンは有効期限まで技術的には使えます。なので、期限を短く設定します。
ログアウト後に攻撃者がアクセストークンを盗んでも、1時間後には完全に無効化されるので、実質的な被害は限定的にすることができます。
しかし、完全にはクライアントとサーバーのセッション(ある期間・一連の活動や集まり)を切れないという課題もあるため、JWTは完璧というわけではなさそうです。
■悪者がJWTを盗んで偽造した場合に検知される仕組み
JWTアクセストークンの偽造検知の仕組み

JWTアクセストークンの偽造検知の仕組みは以下になります。
HeaderをBase64エンコードした値、PayloadをBase64エンコードした値、HeaderとPayloadとSECRET_KEYをくっつけて、HMACを通して作成されたSignatureを作成する。
3つの値を「.」で繋いだアクセストークンをサーバーで発行して、クライアントに返すクライアントは、localStorageやCookieに保存する
- 悪者がJWTアクセストークンを何らかの方法で盗む(ex. XSSなど)
- 悪者がJWTアクセストークンのHeaderやPayloadを、JavaScriptの
atob()関数でデコードして、中身を確認して、Payloadを書き換えて、btoa()関数でエンコードする。(Signatureはatob()関数でデコードしても見れない)
改ざんしたアクセストークンで、APIを叩く。 - サーバー側で、クライアントから送られてきたアクセストークンを「.」で分割して「Header」「Payload」「Signature」に分ける。
クライアントから送られてきた「Header」「Payload(改ざんされている)」「AWSから取得したSECRET_KEY」を使って、「Signature」を再計算する。
再計算した「Signature(Payloadが改ざんされている)」とクライアントから送られてきた「Signature(悪者はSECRET_KEYを知らないので改ざんできない = Payloadは元の{"user_id": 123}のまま)」で比較する - 比較した結果、「Signature」が異なるので、改ざんされていると検知できる。
悪者ができることことは、Payloadを書き換えることしかできないです。
悪者はSECRET_KEYを知らないため、改ざんしたSignatureを作成できないことになります。
もし、SECRET_KEYが漏洩すると、悪者はPayloadを改ざんできますし、Signatureも自分で計算して改ざんできることになります。
その結果、完璧な偽造JWTトークンを作成できて、誰にでもなりすませる事ができるようにになります。
このようなことにならないために、以下の点を注意しないといけません
- コードに直接SECRET_KEYを書かない
- SECRET_KEYをGitHubにpushしない
- ログに出力しない
- フロントエンドにSECRET_KEY渡さない
SECRET_KEYを守るために、以下のような対策をする必要があります。
■まとめ
JWT認証の仕組みや改ざんに対してどのように防ぐのかがわかって、すごく勉強になりました。 理論はわかったので、今後は実践ベースに落とし込んだり、その他の認証方式についても知見を広げていければと思います。
ここまで読んでくださってありがとうございました。
弊社ではエンジニアを募集しています!少しでもご興味がありましたら、ぜひカジュアル面談でお話しましょう!