はじめに
今回は認証機能の学習としてその機能のひとつ「JWT」について
概要レベルですがアウトプットしていきます。
JWTとは
Json Web Token(JWT)とはクライアント・サーバ間で安全に情報を共有するための規格の一つです。
共有される情報はJSON形式が含まれています。
署名付きトークンのため、署名時に使用した鍵で
その情報が悪意のある第三者に改ざんされていないかどうか検証することも可能です。
Header / Payload / Signatureで構成される
JWTのトークンにはドット(. )で区切られた3つの部分に分かれています。
例:xxxxx.yyyyy.zzzzz
Header
ヘッダーは、トークンタイプ(JWT)と使用されている署名アルゴリズム(HMAC SHA256 またはRSAなど)の2つの部分で構成されます。
例えば:
{
"alg": "HS256",
"typ": "JWT"
}
次に、このJSONはBase64Urlでエンコードされ、JWTの最初の部分を形成します。
Payload
トークンの2番目の部分は、Claimsを含んだペイロードです。
Claimsは、エンティティ(通常はユーザー)と追加データに関するステートメントです。
ClaimsはRegistered claims、Public Claims, Private Claimsの3種類があります。
Regisered Claims
これらは、一連の有用で相互運用可能なクレームを提供するために、
必須ではありませんが使用することを期待されて提供されています。
iss : トークン発行者
sub : JWTの主語となる主体の識別子
aud : JWTを利用する受信者を識別する
exp : JWTの有効期限
nbf : JWTが有効になる日時を示す
iat : JWTを発行した時刻を示す
jti : JWTのための一意な識別子を提供する
Public Claims
JWTの利用者によって自由にていぎすることができます。
しかし、衝突を回避するにはIANA JSON Web トークンレジストリで定義するか、
あるいは耐衝突性を持つ名前空間を含むパブリック名にする必要があります。
Private Claims
これらは、それらの使用に同意し、Registered ClamsでもPublic Claimsでもない関係者間で情報を共有するために作成されたカスタムクレームです。
ペイロードの中身の例として以下のようになっています。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
ペイロードはBase64Urlにエンコードされて、JSON Web Tokenの2番目の部分を形成します。
注意
著名付きトークンの場合、情報自体は改ざんから保護されるが、だれでも読み取りは可能です。
暗号化されていないかぎり、ペイロードやヘッダーに機密情報などは盛り込まないように注意が必要になります。
Signature
署名部分を作成するには、エンコードされたヘッダー、エンコードされたペイロード、シークレット、
ヘッダーで指定されたアルゴリズムを取得し、署名する必要があります。
例えば、HMAC SHA256アルゴリズムを使用する場合、署名は次のように作成されます。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
署名は、メッセージが途中で変更されていないことを確認するために使用されます。
また、秘密鍵で署名されたトークンの場合は、JWT の送信者が本人であることを確認することもできます。
JWTのメリット・デメリット
メリット
-
Tokenベースの認証はスケーラブルで効率的
トークンはユーザー側で保存する必要があることがわかっているため、
トークンはスケーラブルなソリューションを提供します。
さらに、サーバーは情報とともにトークンを作成して検証するだけで済むため、
Web サイトまたはアプリケーションでより多くのユーザーを一度に維持することが面倒なく可能です。 -
柔軟性とパフォーマンス両方に優れる
トークンベースの認証は複数のサーバーで使用でき、
様々なWeb サイトやアプリケーションの認証を一度に提供できるため、
トークンベースの認証に関しては、柔軟性と全体的なパフォーマンスの向上も可能です。 -
強固なセキュリティ
JWTのようなトークンはステートレスであるため、
トークンの作成に使用されたサーバー側アプリケーションで受け取ったときに、秘密鍵のみがトークンを検証できます。
デメリット
-
1つのキーだけに依存する
JWT は 1 つのキーのみを使用します。
開発者/管理者が適切に処理しないと、機密情報が危険に晒される可能性がある深刻な結果につながってしまいます。 -
データが冗長になる
JWTの全体的なサイズは、通常のセッショントークンのサイズよりもかなり大きいため、
データが追加されるたびに長くなります。
そのため、トークンにさらに情報を追加すると全体的な読み込み速度に影響し、ユーザーエクスペリエンスが妨げられます。 -
寿命が短い
有効期間が短いJWTは、ユーザーが操作するのが難しくなります。
これらのトークンには頻繁な再認証が必要であり、特にクライアントにとっては煩わしい場合があります。
リフレッシュ トークンを追加して適切に保存することが、このシナリオを修正する唯一の方法です。
このシナリオでは、有効期間の長いリフレッシュ トークンによって、ユーザーがより長期間にわたって承認された状態を保つことができます。
Expressで実装してみる
こちらのYouTubeを参考に実装してみました。
気になる方はぜひ試してみてください。
コードを全部載せると記事が見づらくなるので一部抜粋します。
と、言ってもとてもシンプルな内容です。
JWTを使用するためのパッケージとしては主に以下2つをインストールします。
- jsonwebtoken (JWT)
- bcrypt (パスワードのハッシュ化)
今回はUser.jsにて疑似的なデータベースを使用しています。
ユーザー情報を登録してトークンを発行する処理
router.post(
"/register",
body("email").isEmail(),
body("password").isLength({ min: 6 }),
async (req, res) => {
const email = req.body.email;
const password = req.body.password;
// 中略:バリデーションチェックとかありつつ
const token = await JWT.sign(
{
email,
},
"SECRET_KEY",
{
expiresIn: "24h",
}
);
return res.json({
token: token,
});
}
);
jwt.sign( ペイロード、秘密鍵、[ オプション、コールバック ] ) にて、Emailと秘密鍵を使用してJWTトークンを発行しています。オプション部分でトークンの有効期限を設定することもできます。
ログインしてきたユーザーに対してサインイン済みのユーザーか認証してトークン発行
router.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = User.find((user) => user.email === email);
if (!user) {
return res.status(400).json([
{
message: "そのユーザーは存在しません。",
},
]);
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json([
{
message: "パスワードが異なります。",
},
]);
}
const token = await JWT.sign(
{
email,
},
"SECRET_KEY",
{
expiresIn: "24h",
}
);
return res.json({
token: token,
});
})
ソース全体はGithubの方にあげています。
localStrageには絶対に保存してはならない
発行したトークンの保存先としてどこに保存したらいいか迷うと思いますが
localStorageには絶対に保存してはいけません。理由としては 「セキュアではないから」 です。
クロスサイトスクリプティング攻撃(XXS)の被害にあってしまう可能が高くなります。
localStrageの代替手段として cookieに保存するパターン がシンプルかつ「セキュア」です。
おわりに
今回は概要レベルでのアウトプットになりましたが、
まだ細かな知識までは理解が及んでいない部分があります。
引き続き精進していきます。
また、別の記事にてインプットした他の認証方式をご紹介させて頂きつつ、認証部分の知識を深めていこうと思います。
参考リンク