IDとパスワードのみに依存した認証システムは、情報漏洩や不正アクセスのリスクを常に抱えています。この問題を解決する強力な手法が「多要素認証(MFA)」です。
本記事では、サーバーに状態を持たないステートレスなMFAフローを支える認証トークンとして、JWT(JSON Web Token)を活用する方法を解説します。具体的なフローからセキュリティ上の注意点、そしてリフレッシュトークンなどの実践的な設計パターンまで網羅的に扱い、現代的なアプリケーション開発に求められる認証基盤の知識を提供します。
また最後のPythonのプログラムのサンプルソースも掲示します。
- JWTを使い、サーバーに負荷をかけない「ステートレス」な多要素認証の方法を説明します。
- HMACやリフレッシュトークンを活用し、安全で実践的な認証システムを構築するための鍵管理のポイントを説明します。
- Pythonのサンプルソースで実装方法がわかります。
この記事のプログラムは、以下の環境で開発および動作確認を行っています
・OS: Windows11
・言語: Python 3.13
・Webフレームワーク: Flask 3.1.1
なぜIDとパスワードだけでは危ないの?多要素認証(MFA)が必要な理由
ログイン情報の漏洩リスク
IDとパスワードのみの認証が危険な理由は、その情報が漏洩する可能性を常に内包しているためです。漏洩の原因は多岐にわたります。
- パスワードリスト攻撃: 他のサービスから漏洩したIDとパスワードのリストを利用して、不正ログインを試みる攻撃です。ユーザーが複数のサービスでパスワードを使い回している場合、一つのサービスからの漏洩が他のサービスへの不正アクセスに直結します。
- ブルートフォース攻撃・辞書攻撃: 単純なパスワード(例:
password123)は、ツールによって機械的に試行され、短時間で突破される可能性があります。 - フィッシング詐欺: 正規のサイトを装った偽のログインページへユーザーを誘導し、IDとパスワードを窃取する手口です。
これらの攻撃手法が存在する以上、IDとパスワードだけ(知識情報だけ)に依存する単要素認証には構造的な限界があります。そこで必要となるのが多要素認証です。
多要素認証(MFA)の概要
多要素認証(Multi-Factor Authentication, MFA)とは、ログイン時に複数の異なる種類の要素を要求する認証方式です。認証要素は、以下の3つに大別されます。
- 知識情報 (Something you know): ユーザーだけが知っている情報(パスワード、PINコードなど)。
- 所持情報 (Something you have): ユーザーだけが持っている物理的なモノ(スマートフォン、ハードウェアトークンなど)。
- 生体情報 (Something you are): ユーザー自身の身体的特徴(指紋、顔認証など)。
MFAは、これらの要素の中から2つ以上を組み合わせて本人確認を行います。例えば、「ID/パスワード(知識情報)」に加えて「スマートフォンに届く確認コード(所持情報)」を要求する仕組みは、二要素認証(2FA)と呼ばれ、MFAの代表的な実装例です。
MFAがセキュリティを向上させる仕組み
MFAがセキュリティを向上させる本質は、攻撃者が突破すべき認証要素の種類を増やす点にあります。
パスワードリスト攻撃によって攻撃者がIDとパスワード(知識情報)を窃取したとしても、MFAが導入されていれば、第二の要素である「ユーザーのスマートフォン(所持情報)」がなければログインを完了できません。オンラインで完結する情報の窃取と、物理的なデバイスの窃取では、攻撃の難易度が全く異なります。
このように、MFAは「たとえ一つの要素が侵害されても、他の要素が防御壁として機能する」という多層防御を実現します。これにより、単要素認証と比較してセキュリティレベルを飛躍的に高めることができるのです。
「ステートレス認証」とJWTの仕組み
「ステートフル」と「ステートレス」の概念
認証情報をサーバー側でどう管理するかによって、システムは「ステートフル」と「ステートレス」に大別されます。
なじみがあるプロトコル名で説明すれば、ファイル転送で何度もコマンドを打つ必要があるFTPはステートフルなプロトコルです。Webを閲覧するHTTPは状態を保持じないので、ステートレスなプロトコル、と言えます。
ステートフル(Stateful) サーバーがユーザーのログイン状態(セッション情報)を自身のメモリやストレージに保持する方式です。一度ログインしたユーザーはサーバーに記憶されているため、後続のリクエストではセッションIDを照合するだけで済みます。しかし、アクセス増加に伴いサーバーを複数台にスケールアウトさせるような際は、全サーバー間でセッション情報を同期する必要があります。
ステートレス(Stateless) サーバーがユーザーのログイン状態を一切保持しない方式です。サーバーは各リクエストを独立したものとして扱います。その代わり、ユーザー側がリクエストの都度、自身の認証情報を含む「認証トークン」を提示します。サーバーはそのトークンを検証するだけで認証を完了できます。サーバーが状態を持たないため、スケールアウトが容易であり、現代的なマイクロサービスアーキテクチャなどに適しています。このステートレス認証を実現する上で中心的な役割を担うのがJWTです。
【実装の選択肢】ステートレス vs 短期的なステートフル
今回のログイン時のステートレス設計は、サーバー間で状態を共有するのが難しい場合に有効です。一方で、単一のサービスやシンプルな構成の場合、OTPや関連情報をRedisのようなインメモリの短期ストレージに数分間だけ保存する「短期的なステートフル」設計の方が、実装が単純で堅牢になることもあります。どちらの設計を選択するかは、システムの要件や規模に応じて判断することが重要です。
JWT(JSON Web Token)の構造
JWT(JSON Web Token)は、自己完結型(self-contained)の認証情報を持つトークンで、JSON形式のデータを内包し、改ざんを検知するためのデジタル署名が付与されています。xxxxx.yyyyy.zzzzz のようにピリオドで区切られた3つのパートで構成されます。
先に伝えておきますが、JSON特有の中カッコ{}が見えないのは、Base64URLエンコードがされている為です。各パートの間の「.」を抜いてデコードすれば、ちゃんとJSON形式のデータになりますからね。
[図解: JWTの構造 (ヘッダー、ペイロード、署名)]
xx.yy.zz
││ └署名(秘密鍵で生成)
│ ペイロード(user_id,exp等)
ヘッダー(alg: HS256, typ: JWT)
- ヘッダー (Header): トークンの種類(JWT)や署名アルゴリズム(例: RS256)などのメタデータを含みます。
- ペイロード (Payload): ユーザーID、有効期限、権限情報など、アプリケーションが利用する具体的なデータを含みます。これらの情報はClaim(クレーム)と呼ばれます。
- 署名 (Signature): ヘッダーとペイロードを、サーバーだけが持つ秘密鍵で署名したものです。
詳しくは、ペイロードをBASE64URLエンコードし、ヘッダーをBASE64URLエンコードし、これらを.(ドット)で繋ぎ、この全体を暗号化し、さらにBASE64URLエンコードするしたものが署名です。
この署名により、トークンが正当な発行者によって生成されたこと、そして途中で改ざんされていないことを検証できます。
ヘッダーとペイロードはBase64URLエンコードされているだけであり、暗号化はされていません。そのため、何等かの方法でJWTを誰かが取得したら、その人はデコードして中身を閲覧できますので、パスワードなどはJWTに入れるを含めてはいけません。JWTのセキュリティは、情報の秘匿性ではなく、署名による完全性(Integrity)によって担保されています。なお、ペイロードに含まれる情報を第三者から秘匿したい場合は、JWTを暗号化するJWE(JSON Web Encryption)という関連仕様も存在します。

JWTがステートレス認証に最適な理由
JWTは、認証に必要な情報をすべてトークン自身が保持しているため、ステートレス認証に最適です。
ステートレスなサーバーは、ユーザーからのリクエストに含まれるJWTを受け取ると、その署名を検証するだけで以下の確認が完了します。
- トークンの発行者が正当であること(署名の検証)
- トークンが改ざんされていないこと(署名の検証)
- トークンが有効期限内であること(ペイロードの
expクレームの検証) - リクエストの主体が誰であるか(ペイロードの
subクレームなどの検証)
サーバーはセッション情報をデータベースなどに問い合わせる必要がなく、トークンの検証のみでリクエストを処理できます。この特性により、サーバーの負荷が軽減され、システムの拡張性が大幅に向上します。
署名作成時の鍵は、公開鍵方式の秘密鍵か、共通鍵方式の秘密鍵か
JWTでは、共通鍵方式と公開鍵暗号方式の両方が使えます。どちらを選ぶかは、システムの要件や構成によって決まります。
1. 共通鍵方式 (Symmetric Key)
これは、1つの共通の鍵(秘密鍵)を「署名の作成」と「署名の検証」の両方に使用する方法です。最もシンプルで一般的な方法です。
共通鍵暗号方式は主に次の環境で使用します。
- 単一のアプリケーション内で認証を完結させる場合や、
- サーバー間で秘密鍵を安全に共有できる信頼された環境なら、こちらの鍵で署名をします。
2.公開鍵暗号方式 (Asymmetric Key)
「秘密鍵」と「公開鍵」のペアを使用する方法です。
署名の作成: 秘密鍵を使ってJWTに署名しますが、秘密鍵は発行者サーバーだけが厳重に保管します。
署名の検証: 公開鍵を使って署名が正しいかを検証しますが、公開鍵は検証を行う誰にでも(他のサービスやクライアントにも)配布して問題ありません。
公開鍵暗号方式は主に次の環境で使用します。
- 認証サーバー(署名作成する)とリソースサーバー(署名検証する)が分離している場合(例: GoogleなどのIdPを利用したログイン)
- APIの利用者がそのAPIから発行されたJWTが本物であることを確認したい場合は、公開鍵暗号方式の鍵を使用します。
つまりは、複数の場所に秘密鍵を配りたくない場合は、公開鍵暗号方式を採用するのです。
なお、今回のサンプルコードは、秘密鍵を複数の場所に配る必要はないので、共通鍵を採用しています。
JWTなんて大層な名前が付けているけど単に署名付きのJSONでしょ?
実際のところ、JWTがやっていることの核心は 「署名付きのJSONデータを標準フォーマットで表現している」 ただそれだけです。しかしJWTが広まった理由はそれなりにあります。
「署名」と「JWT」の違い
- 署名だけ
- 任意のデータに対して、秘密鍵や証明書を使って署名を付与する。
- フォーマットやエンコード方法は自由。受け取り側も同じルールを知っていないと検証できない。
- JWT
header.payload.signatureという形式で、世界共通の「署名付きJSONトークンの型」が決まっている。- Base64URLでエンコードされ、HTTPヘッダやURLパラメータにそのまま載せられる。
alg(署名アルゴリズム)、exp(有効期限)、iss(発行者)、aud(利用者)といった共通フィールドの意味が標準化されている。
結局、JWTの正体は、「ただの署名」を他人とやり取りするための国際標準フォーマットにしたものなのですが、標準フォーマットであることで、次のメリットが得られます。
- 世界中のライブラリがそのまま使える(自作不要)。
- システム間連携で「署名の付け方・フォーマット」を相談する必要がない。
- ペイロードに共通的な意味を持たせられる(
subはユーザーID、expは有効期限、など)。
もちろん、「JWTを使うよりもシンプルに署名だけで十分なケース」はあります。たとえば 単一部署だけで構築しているAPI通信 なら「署名」だけで十分のケースも多いでしょう。
逆に マルチサービス連携 や OAuth/OpenID Connectのような他者とのやり取りでは、JWTのように標準化がないと、相手と一々仕様をすり合わせる必要が出てきて、膨大な時間を浪費する事になるでしょう。
JWTを使った多要素認証の実装フロー
ステップ1:第一認証と一時トークンの発行
ステートレスMFAの実装では、役割の異なる2種類のJWTを使い分けることが重要です。
- 第一認証: ユーザーがIDとパスワードをサーバーに送信し、サーバーはデータベースの情報と照合して第一要素(知識情報)の認証を行います。
- HMACによるOTPハッシュの生成: 認証が成功すると、サーバーはOTP(One Time Password)を生成します。次に、OTPのハッシュ値をHMAC(Hash-based Message Authentication Code)を用いて計算します。この計算には、OTP検証専用の秘密鍵(otp_secret)を利用してください。
otp_hash = HMAC(secret=otp_secret, message=otp, algorithm=SHA256)
- 一時トークンの生成・発行: サーバーは、上記で計算した
otp_hashをペイロードに含んだ「一時トークン」をJWTとして生成します。このトークンには、ユーザーIDとOTPのハッシュ値が含まれます。 - OTPメール送信: 同時に、サーバーは生のOTPをユーザーの登録メールアドレスなどに送信します。
【重要】Salt (ソルト)とHMACの役割、および鍵の分離
OTP(One Time Password)を保護する際、Salt(ソルト)とHMACは混同されがちですが、役割は異なります。
- Salt (ソルト): 同じOTPであっても毎回異なるハッシュ値を生成できるようにするための追加データです。これにより、攻撃者があらかじめ作成したレインボーテーブルを使って効率的に照合することを防ぎます。ソルト自体は秘密である必要はなく、あくまで「多様性を付与する仕組み」として機能します。
- HMAC(Hash-based Message Authentication Code):秘密鍵を混ぜ込んでハッシュを計算する仕組みです。攻撃者は秘密鍵を知らないため、オフラインでハッシュ値を総当たりしたり、レインボーテーブルを用意することができません。
鍵の用途分離
HMACで利用する秘密鍵を、他の仕組み(例えばJWTの署名鍵など)と兼用することは技術的には可能です。しかしセキュリティの基本原則である「鍵の用途分離」に従い、OTP検証専用の秘密鍵を用意することが推奨されます。用途ごとに鍵を分けることで、仮にどちらかの鍵が漏洩しても、影響が他の仕組みに波及しないようにできます。
ステップ2:第二認証とアクセストークンの発行
次に、ユーザーは受け取ったOTPを使って第二認証を完了させます。
- クライアント: ユーザーがメールなどで受け取ったOTPをアプリケーション画面に入力し、ステップ1で受け取った「一時トークン」と共にサーバーへ送信します。
※クライアントのブラウザはJWTの中身(ペイロードや署名)を一切解釈・検証しません。ブラウザの役割は、サーバーから受け取ったJWTという「通行証」を、次のリクエスト時に「私はこれを持っています」とサーバーに提示するだけです。 - サーバー: 受け取った一時トークンの署名と有効期限を検証し、ペイロードからユーザーIDとOTPハッシュ値を取り出します。
- サーバー: サーバー内部で保持しているOTP検証用の秘密鍵を使い、クライアントから送られてきた生のOTPをHMACでハッシュ化します。
calculated_hash = HMAC(secret=otp_secret, message=otp, algorithm=SHA256)
- サーバー:
calculated_hashが、トークンから取り出したOTPハッシュ値と完全に一致するかを比較します。 - サーバー: 検証が成功すれば、MFA成功と判断し、ユーザーの権限情報や有効期限を含む「アクセストークン」を新たにJWTとして生成し、クライアントに返却します。
以降、クライアントはこのアクセストークンをAPIリクエストの都度提示し、サーバーはそれを検証することで認証・認可を行います。
2種類のトークンを使い分ける理由
一時トークンとアクセストークンを分離する理由は、権限の分離(Separation of Privilege)のためです。
一時トークンは、第二認証を完了させる目的のためだけに存在し、APIアクセスなどの権限は一切持ちません。これにより、万が一IDとパスワードが漏洩しても、攻撃者は一時トークンしか取得できず、実質的な被害を防ぐことができます。
アクセストークンは、全ての認証をクリアしたことを証明する強力なトークンであり、具体的な操作権限を持ちます。そのため、発行条件はMFAの完了を必須とすることで、セキュリティを担保します。この厳格な権限分離が、ステートレスMFAの安全性の根幹をなします。
Python + Flaskのサンプルコード
シンプルなPython+Flaskアプリケーションです。これで一連の動きを表現しています。
# -*- coding: utf-8 -*-
from flask import Flask, request, render_template_string, redirect, url_for
import jwt
import hmac
import hashlib
import datetime
from datetime import timezone
import random
app = Flask(__name__)
# ■■■ 重要 ■■■
# JWT署名用とOTPハッシュ化用に、必ず異なる秘密鍵を使用する
JWT_SECRET = "jwt-secret-key-for-signing"
OTP_SECRET = "otp-secret-key-for-hashing"
# --- メモリ内ストア(デモ用) ---
# 実際のアプリケーションではデータベースを使用してください
users = {"alice": "password123"}
# --- シンプルHTMLテンプレート ---
login_page_html = """
<!DOCTYPE html>
<html>
<head>
<title>ログイン</title>
</head>
<body>
<h2>ステップ1: ログイン</h2>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
<form method="post" action="/login">
ユーザー名: <input name="username" value="alice"><br>
パスワード: <input type="password" name="password" value="password123"><br>
<input type="submit" value="ログイン">
</form>
</body>
</html>
"""
verify_page_html = """
<!DOCTYPE html>
<html>
<head>
<title>OTP検証</title>
</head>
<body>
<h2>ステップ2: OTP (ワンタイムパスワード) の入力</h2>
<p>OTPがメールで送信されました(このデモでは以下に表示されます)。</p>
<p><strong>デモ用OTP: {{ otp }}</strong></p>
<form method="post" action="/verify">
<input type="hidden" name="temp_token" value="{{ temp_token }}">
OTP: <input name="otp"><br>
<input type="submit" value="検証">
</form>
</body>
</html>
"""
result_page_html = """
<!DOCTYPE html>
<html>
<head>
<title>認証結果</title>
</head>
<body>
<h2>認証結果</h2>
{% if access_token %}
<h3>アクセストークン</h3>
<textarea rows="8" cols="70" readonly>{{ access_token }}</textarea>
<p>このトークンを使用して、保護されたリソースにアクセスできます。</p>
{% else %}
<h3 style="color: red;">エラー</h3>
<p>{{ error }}</p>
{% endif %}
<br><br>
<a href="/">最初からやり直す</a>
</body>
</html>
"""
# --- ルート ---
@app.route("/")
def index():
return redirect(url_for("login"))
# --- ステップ1: ログイン & 一時トークン発行 ---
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
# ユーザー認証
if users.get(username) != password:
return render_template_string(login_page_html, error="ユーザー名またはパスワードが違います。")
# 1. OTPを生成
otp = str(random.randint(100000, 999999))
# 2. OTPのハッシュをHMACで計算 (ブログ記事で説明されている通り)
otp_hash = hmac.new(OTP_SECRET.encode('utf-8'), otp.encode('utf-8'), hashlib.sha256).hexdigest()
# 3. 一時トークンを作成 (ペイロードにユーザー名とOTPハッシュを含む)
temp_token_payload = {
'sub': username,
'otp_hash': otp_hash,
'exp': datetime.datetime.now(timezone.utc) + datetime.timedelta(minutes=5) # 短い有効期限
}
temp_token = jwt.encode(temp_token_payload, JWT_SECRET, algorithm="HS256")
# 4. ユーザーにOTPを送信 (このデモでは画面に表示)
# 実際のアプリでは、このOTPをメールやSMSで送信します。
return render_template_string(verify_page_html, otp=otp, temp_token=temp_token)
return render_template_string(login_page_html)
# --- ステップ2: OTP検証 & アクセストークン発行 ---
@app.route("/verify", methods=["POST"])
def verify():
temp_token = request.form.get("temp_token")
otp_from_user = request.form.get("otp")
if not temp_token or not otp_from_user:
return render_template_string(result_page_html, error="一時トークンまたはOTPがありません。")
try:
# 1. 一時トークンをデコードして検証
payload = jwt.decode(temp_token, JWT_SECRET, algorithms=["HS256"])
print(payload)
username = payload.get('sub')
otp_hash_from_token = payload.get('otp_hash')
# 2. ユーザーが入力したOTPからハッシュを計算
expected_hash = hmac.new(OTP_SECRET.encode('utf-8'), otp_from_user.encode('utf-8'), hashlib.sha256).hexdigest()
# 3. トークン内のハッシュと計算したハッシュを比較
if hmac.compare_digest(otp_hash_from_token, expected_hash):
# 4. 検証成功。最終的なアクセストークンを発行
access_token_payload = {
'sub': username,
'scope': 'read write', # 例: 権限スコープ
'exp': datetime.datetime.now(timezone.utc) + datetime.timedelta(hours=1) # 長めの有効期限
}
access_token = jwt.encode(access_token_payload, JWT_SECRET, algorithm="HS256")
return render_template_string(result_page_html, access_token=access_token)
else:
return render_template_string(result_page_html, error="OTPが正しくありません。")
except jwt.ExpiredSignatureError:
return render_template_string(result_page_html, error="一時トークンの有効期限が切れました。もう一度ログインからやり直してください。")
except jwt.InvalidTokenError:
return render_template_string(result_page_html, error="無効なトークンです。")
if __name__ == "__main__":
print("サーバーを http://127.0.0.1:5001 で起動します")
app.run(port=5001, debug=True)
上記pythonプログラムをxxx.pyファイルに保存して、python xxx.pyで実行してください。
その後、ブラウザから「http://127.0.0.1:5001」にアクセスして動作させてみてください。
次の順番で動作を確認できます。
画面イメージ




トークンの 内容確認
一時トークンとアクセストークンの中身も確認しておきましょう。トークンは、Base64URLでエンコードしてありますから、デコードする必要があります。googleの無料Webツールの「Google Admin Toolbox」で簡単にデコードできますので、こちらでトークンの中身を確認します。
https://toolbox.googleapps.com/apps/encode_decode/
①まず一時トークンを確認します。
ステップ2のブラウザ画面で右クリックしてHTMLソースを表示し、<input type=”hidden” name=”temp_token” value=”xxxxx”>の長い文字列xxxxをコピーして、上記URLのテキストボックスに張り付けてください。
また、「Base64Urlデコード」を選択してください。
注意点としては、このデコードツールは、トークンのパートをつなぐ「.」(ドット)は自動的に消してくれませんので、自分で削除してください。今回は見やすく改行に変えています。
最後に送信ボタンを押下してください。
今回のトークン:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsIm90cF9oYXNoIjoiYWVhMWQ3YzAzNTVmYTI0YTRlNmEzZTY5YTVkMTdmMWUzYzBlYWYzZGFlZGQzMjk3MzExNDJlZGQ1NTkxNTRhMCIsImV4cCI6MTc1NzExOTE2NX0.TDWIWX_Xrj5Zgk9QyCSvRmCfrSt_phCHtZ3F1y-t2Mk

結果は画面下に表示されます。ちゃんとJSON形式で値が見えますね。otp_hashも見えています。最後のパートは署名欄でバイナリデータですので、文字化けして見えます。
②アクセストークンも確認します。
検証結果画面に表示してあるアクセストークンを張り付けてください。トークンのパートをつなぐ「.」(ドット)が2つ紛れていますで、これを手動で削除する事はわすれずに。
今回のトークン:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInNjb3BlIjoicmVhZCB3cml0ZSIsImV4cCI6MTc1NzEyMjQ2OX0.KnaZ-vSIwetpF0knDpZxYf7MWR57zDCFvvN3c-Z2Da4

アクセストークンも、scope等の値がJSON形式で値が見えますね。最後のパートは署名欄でバイナリデータですので、文字化けします。
実践的なトークン管理と今後の展望
アクセストークンのライフサイクル管理:リフレッシュトークンの活用
アクセストークンの有効期限を不必要に長く設定すること(例:24時間)は、トークンが漏洩した際のリスクを高めます。そのため、現代のベストプラクティスでは「短命なアクセストークン」と「長命なリフレッシュトークン」を組み合わせる方式が推奨されます。
- アクセストークン: 有効期限を数分〜1時間程度と短く設定します。これにより、万が一漏洩しても、不正利用される時間を限定できます。
- リフレッシュトークン: アクセストークンよりも長い有効期限(数日〜数週間)を持ちます。このトークンの唯一の役割は、新しいアクセストークンを再発行することです。
ユーザーはアクセストークンの有効期限が切れた際、リフレッシュトークンを使ってサーバーに新しいアクセストークンを要求します。これにより、ユーザーは頻繁に再ログインする必要がなく、利便性を損なわずにセキュリティを向上させることができます。
今後の展望:パスワードレス認証とWebAuthn
本記事ではメールOTPを例に解説しましたが、MFAはさらに進化しています。その最前線にあるのが、FIDO AllianceとW3Cによって標準化されたWebAuthnです。
WebAuthnは、デバイスに搭載された生体認証(指紋、顔)や物理セキュリティキー(YubiKeyなど)を活用し、公開鍵暗号方式に基づいて安全な認証を実現します。これにより、フィッシングに強く、サーバー側にパスワードを保存する必要がない「パスワードレス認証」が可能になります。
今後は、従来のパスワードベースのMFAから、WebAuthnのような、より安全で利便性の高い認証方式への移行が加速していくと考えられます。
JWT署名鍵の生成と管理
JWTへの署名の暗号方式
JWTの署名方式には、単一の秘密鍵を共有する「共通鍵暗号方式(HS256など)」と、秘密鍵と公開鍵のペアを使う「公開鍵暗号方式(RS256など)」があります。マイクロサービスアーキテクチャのように複数のサービスが連携する環境では、公開鍵暗号方式(RS256)が推奨されますが・・。
しかし今回は共通鍵を利用します。JWTの使い方として、署名する側、検証する側が同じサーバーを想定している為です。
共通鍵は、サーバーの環境変数に入れるなど、適切に管理するなどして下さい。
<参考>OpenSSLによる鍵ペアの生成
参考までに、公開鍵を利用してJWT署名用の鍵ペアを作成する場合の、opensslコマンドを掲載します。
1. 秘密鍵の生成 以下のコマンドで、2048ビットのRSA秘密鍵を生成します。
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
2. 公開鍵の抽出 次に、生成した秘密鍵からペアとなる公開鍵を抽出します。
openssl rsa -pubout -in private_key.pem -out public_key.pem
TLS証明書の鍵をJWTの署名に使えば楽じゃないの?
WebサーバーがHTTPS通信で使用しているTLS証明書の鍵ペアを、JWTの署名に再利用することは、技術的には可能ですが、セキュリティ上のベストプラクティスに反するため避けるべきです。
- 鍵の役割(責務)の明確な分離: TLSの鍵の責務は「通信経路の暗号化」、JWT署名鍵の責務は「認証情報の完全性保証」であり、目的が全く異なります。
- ライフサイクルのミスマッチ: TLS証明書の有効期間は90日程度と短く、頻繁な更新が必要です。鍵を更新した際、古い鍵で署名された有効なJWTがすべて無効となり、大規模な障害を引き起こします。
- 影響範囲の拡大: 万が一、共用している秘密鍵が漏洩した場合、通信の盗聴と認証トークンの偽造の両方が可能となり、システムのセキュリティが完全に崩壊します。
まとめ
本記事では、JWTをステートレスなMFAフローの基盤として活用する方法について、多角的に解説しました。
- MFAの重要性: 単要素認証のリスクを回避し、セキュリティを向上させるためにはMFAが不可欠です。
- ステートレスとJWT: 拡張性の高いシステム構築にはステートレスアーキテクチャが有効であり、JWTはその中核技術です。
- 実践的なトークン設計: 「一時トークン」と「アクセストークン」の分離に加え、「リフレッシュトークン」を活用することで、利便性と安全性を両立できます。
これらの原則に基づいた設計と実装を行うことで、安全でスケーラブルな認証基盤を構築することができます。


