はじめに
認証方式の1つであるJWTについてのまとめと使用例
JWTとは
JSON Web Tokenの略
認証情報を含むJSONをbase64エンコードしたものに署名を付与したもの
利用例
- クライアント側から認証情報(例:ユーザー名、パスワード)をサーバーに送信
- サーバー側で認証情報を確認し、認証OKの場合JWTを発行し、クライアント側に返却
- クライアントは次回以降、JWTを付与したリクエストを送信
- サーバー側はJWTを検証する
なお、JWTの暗号化アルゴリズムは大きく分けて2種類ある。
-
共通鍵方式
HS256というアルゴリズムを使用する。
認証サーバとリソースサーバが同じ場合はこの方式が使われることが多い。 -
公開鍵/秘密鍵方式
RS256というアルゴリズムを使用する。
認証サーバとリソースサーバが別々の場合にこの方式が使われる。
認証サーバに秘密鍵、リソースサーバに公開鍵が配置される。
JWTの構造
JWTの例は以下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFyb3UiLCJpYXQiOjE1OTIyNDAxODF9.vgsytL2KiAp-LXFSSmVXObia0bStoZqOCdYoEXdRaz8
※JWT公式に貼り付けると内容がわかる
形式は[ヘッダー].[ペイロード].[署名]となる。
ヘッダー
{
"alg": "HS256",
"typ": "JWT"
}
署名の暗号化方式とトークンの種類を設定
ペイロード
{
"user": "tarou",
"iat": 1592240181
}
実際のデータの中身
base64エンコードしているだけなので、パスワードとかの重要情報を含んではいけない。
上記の例以外にもトークンの有効期限や発行者などの情報を設定することもできる。
署名
ヘッダーとペイロードを鍵で暗号化した値
鍵はサーバー側で管理しておく。
署名を検証することによって、データの改ざんが行われていないかチェックすることができる。
実際に使ってみた
Node.js/ExpressでAPIを作ってみる。
作成するAPIは以下2つ
- JWT発行API
- 認証必須API
今回は共通鍵方式によるJWTで認証を実現する。
ソースの説明
全体像が分かったほうがいい方のために、ソース全部貼ります。
// ➀おまじない
const express = require("express");
const jwt = require("jsonwebtoken");
const PORT = 3000;
const app = express();
app.use(express.json())
app.use(express.urlencoded({ extended: true }));
// ➁鍵
const SECRET_KEY = "abcdefg";
// ➂JWT発行API
app.post('/login', (req, res) => {
// 動作確認用に全ユーザーログインOK
const payload = {
user: req.body.user
};
const option = {
expiresIn: '1m'
}
const token = jwt.sign(payload, SECRET_KEY, option);
res.json({
message: "create token",
token: token
});
});
// ➃認証用ミドルウェア
const auth = (req, res, next) => {
// リクエストヘッダーからトークンの取得
let token = '';
if (req.headers.authorization &&
req.headers.authorization.split(' ')[0] === 'Bearer') {
token = req.headers.authorization.split(' ')[1];
} else {
return next('token none');
}
// トークンの検証
jwt.verify(token, SECRET_KEY, function(err, decoded) {
if (err) {
// 認証NGの場合
next(err.message);
} else {
// 認証OKの場合
req.decoded = decoded;
next();
}
});
}
// ➄認証必須API
app.get('/user', auth, (req, res) => {
res.send(200, `your name is ${req.decoded.user}!`);
});
// ➅エラーハンドリング
app.use((err, req, res, next)=>{
res.send(500, err)
})
// ➆サーバ起動
app.listen(PORT, () => console.info('listen: ', PORT));
ソース内の項番に沿って、説明します。
-
➀おまじない
「おまじない」という表現はあまり好きではないが、とりあえずここはExpressでサーバーを立ち上げるための記述なので、飛ばします。 -
➁鍵
暗号化に使用する鍵
本来であれば、環境変数や別ファイルで管理すべきだが、今回は動作確認が目的なのでべた書き -
➂JWT発行API
クライアント側はこのAPIを呼んで、JWTを発行してもらう。
ここでは、有効期限が1分のJWTを発行して、レスポンスに含める。 -
➃認証用ミドルウェア
次にクライアント側からJWTが送られてきた際に、検証を行うミドルウェアを作成する。
今回はリクエストヘッダのauthorizationにBearerスキームで送られてくる想定。
ここでは、以下のケースで場合分けしている。
「トークンがない場合」:➅エラーハンドリングに飛ぶ
「トークン認証NGの場合」:➅エラーハンドリングに飛ぶ
「トークン認証OKの場合」:➄認証必須APIに飛ぶ -
➄認証必須API
クライアント側は➂JWT発行APIで発行されたJWTをリクエストヘッダのauthorizationにBearerスキームで設定してこのAPIを呼ぶ。
app.get()の第二引数で➃認証用ミドルウェアを指定しているので、まずトークンの検証が行われ、認証OKの場合のみステータス200のレスポンスが返される。 -
➅エラーハンドリング
Expressの技術なので、詳しくは説明しないが、next(XXX)された場合、このエラーハンドリングが使われる。
next()の引数で渡された値がerrに設定され、それをそのままクライアント側に返却している。 -
➆サーバ起動
これもExpressの技術なので、ここでは特に説明しません。
動作確認
APIサーバを起動します。
> node .\index.js
listen: 3000
Postmanを使って動作確認。
トークンが返ってきました!
次にトークンの有効期限が切れないうちに認証必須APIを呼ぶ。
リクエストヘッダのauthorizationにBearerスキームでトークンを設定するのを忘れずに
自分の名前が返ってきました!
認証が成功した証。
ステータス500で無効なトークンとのメッセージが返ってきました!
次に有効期限切れの場合
もう1分経ったので、正しいトークンを送信しても…
期限切れのメッセージ!
完璧ですね。
ちなみに今回生成されたJWT。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFyb3UiLCJpYXQiOjE1OTIzMjUxMzksImV4cCI6MTU5MjMyNTE5OX0.Uoqk6Yz129DKQCcvpSKhAw3Ncjln6ILucWAz_1ZLFhg
これをJWT公式に貼り付けると以下のようになる。
トークンの保存場所とサーバへの送信方法
いろんな方法があるよう
また、後述する脆弱性との兼ね合いもあり、何がベストプラクティスなのかは正直全く分かっていない。
- サーバー側でcookieに保存して、そのままやりとりする
- クライアント側はcookieもしくはweb strageに保存して、必要な場合のみリクエストヘッダに付与する
- Authorizationに設定する場合は、Bearerスキームが一般的?
- リクエストボディに入れてもいい
脆弱性
JWTについて調べていると、脆弱性の指摘について、いろいろな記事を見かけた。
ただ、自分の知識が足らず全てを理解することはできなかったので、以下にメモ程度として残しておく。
- cookieに保存するとCSRFの恐れがある
- web strageに保存するとXSSの恐れがある
- cookie or web strageに保存して、使う場合だけリクエストヘッダに含める
- 有効期限を過ぎるまで無効化する方法がないため、有効期限は極力短くすること
- ということは、セッション管理などでは使えなさそう
- 上記の脆弱性も様々な手法で回避できる?
思ったこと
JWTを理解することはそこまで難しくないし、実際に試すことも簡単だったが、
JWTを使いこなすには、OAuthなどの認証方式や、XSS・CSRFなどの攻撃手法などを理解する必要があり、結構ハードルが高そう。
追記:6/22
今回の記事で紹介したのは、共通鍵暗号化方式を使用したJWTの使い方ですが、以下の記事で公開鍵・秘密鍵暗号化方式を使用した場合のサンプルも作成しましたので、興味がある方は見てみてください。