随分昔に同じ趣旨の記事を書いたのですが、再度書き直してみます(あまり変わってない)。
準備
作業場作成
ひとまず作業場所を確保し、必要なモジュールをインストール。body-parserはもういらない。
mkdir jwt-test
cd jwt-test
npm init -y
npm install express jsonwebtoken
実装前に検証
実装に入る前にJWTの生成と検証のコア部分をワンライナーで検証。
生成
なにやらややこしそうに思うが、[ヘッダ].[ペイロード].[署名]をそれぞれbase64でエンコードしているだけ。
署名部はsecret(ここではmy_secretという文字列)を利用してHS256で署名している。
node -e "console.log(require('jsonwebtoken').sign({username:'hoge'},'my_secret'))"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow
サイン時に、 sign({username:'hoge'},'my_secret',, { expiresIn: '1h' })とすることで有効期限を設定可能。
検証
node -e "console.log(require('jsonwebtoken').verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow','my_secret'))"
{ username: 'hoge', iat: 1611790779 }
メモ
大規模?な利用シーンでは署名と検証に公開鍵技術を利用する場合もあると思うが、jsonwebtokenは対応しているよう。詳しくは本家サイトを見る。
実装
では、簡単なJWTを利用したAPI認証機能を実装する。主な機能は以下の感じ。
- /loginにusername, passwordをPOSTで送信し、認証OKならtoken(JWT)を発行。
- /protectedへのアクセスにはAuthorizationヘッダにBearer+tokenを付与し認証OKならコンテンツを戻す。
- 認証機構はverifyToken()という外部関数とし実装し、app.get('/protecte')のミドルウエアとして適用する。
では実装します。
var express = require('express');
var app = express();
var jwt = require('jsonwebtoken');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.listen(3000, function () {
console.log("App start on port 3000");
})
//認証無しAPI
app.get('/', function (req, res) {
res.json({ status: "OK" });
})
//認証+Tokenの発行
app.post('/login', function (req, res) {
//ID,PW取得
var username = req.body.username;
var password = req.body.password;
//認証
//実際はDB等と連携
if (username === "hoge" && password === "password") {
//token生成(フォマットは適当だが、有効期限を設定)
const token = jwt.sign({ username: username }, 'my_secret', { expiresIn: '1h' });
res.json({
token: token
});
} else {
res.json({
error: "auth error"
});
}
})
//認証有りAPI
app.get('/protected', verifyToken, function (req, res) {
res.send("Protected Contents");
})
function verifyToken(req, res, next) {
const authHeader = req.headers["authorization"];
//HeaderにAuthorizationが定義されているか
if (authHeader !== undefined) {
//Bearerが正しく定義されているか
if (authHeader.split(" ")[0] === "Bearer") {
try {
const token = jwt.verify(authHeader.split(" ")[1], 'my_secret');
//tokenの内容に問題はないか?
//ここでは、usernameのマッチと有効期限をチェックしているが必要に応じて発行元、その他の確認を追加
//有効期限はverify()がやってくれるみたいだがいちおう・・・
if (token.username === "hoge" && Date.now() < token.exp * 1000) {
console.log(token);
//問題がないので次へ
next();
} else {
res.json({ error: "auth error" })
}
} catch (e) {
//tokenエラー
console.log(e.message);
res.json({ error: e.message })
}
} else {
res.json({ error: "header format error" });
}
} else {
res.json({ error: "header error" });
}
}
実装できたら実行。
node index.js
動作確認
curlを利用して動作確認をしてみます。
tokenの取得
まずは認証してtokenを取得します。
curl -s -X POST -H 'Content-Type: application/json' -d '{"username":"hoge","password":"password"}' http://localhost:3000/login
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTEyMDksImV4cCI6MTYxMTc5NDgwOX0.Gy_3r3T-AQG8iL28LE1xzVNMEJDKTtgTyMRiaSNpQiM"}
curl -X POST -d "username=hoge&password=password" http://localhost:3000/login でも可。
tokenの利用
取得したtokenを利用して/protectedにアクセスしてみます。
curl -X GET http://localhost:3000/protected -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTEyMDksImV4cCI6MTYxMTc5NDgwOX0.Gy_3r3T-AQG8iL28LE1xzVNMEJDKTtgTyMRiaSNpQiM"
Protected Contents
うまく表示されました。
その他
リフレッシュはどうする?
調べ中。
expireの違う2つのtokenを発行し1つをrefresh tokenとして使うという実装例も。
https://medium.com/@had096705/build-authentication-with-refresh-token-using-nodejs-and-express-2b7aea567a3a