node + expressでJWT (2021年1月）


随分昔に同じ趣旨の記事を書いたのですが、再度書き直してみます（あまり変わってない）。

## 準備

### 作業場作成

ひとまず作業場所を確保し、必要なモジュールをインストール。body-parserはもういらない。

```bash
mkdir jwt-test
cd jwt-test
npm init -y
npm install express jsonwebtoken
```


### 実装前に検証

実装に入る前にJWTの生成と検証のコア部分をワンライナーで検証。

#### 生成

なにやらややこしそうに思うが、[ヘッダ].[ペイロード].[署名]をそれぞれbase64でエンコードしているだけ。
署名部はsecret（ここではmy_secretという文字列）を利用してHS256で署名している。

```bash
node -e "console.log(require('jsonwebtoken').sign({username:'hoge'},'my_secret'))"

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow
```

>サイン時に、 sign({username:'hoge'},'my_secret',, { expiresIn: '1h' })とすることで有効期限を設定可能。


#### 検証

```bash
node -e "console.log(require('jsonwebtoken').verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTA3Nzl9.MbcALpRUEu9KxGZ5S1qLoieb41_dr-i2o__QVnlVTow','my_secret'))"

{ username: 'hoge', iat: 1611790779 }
```

### メモ

大規模？な利用シーンでは署名と検証に公開鍵技術を利用する場合もあると思うが、jsonwebtokenは対応しているよう。詳しくは[本家サイト](https://github.com/auth0/node-jsonwebtoken)を見る。

## 実装

では、簡単なJWTを利用したAPI認証機能を実装する。主な機能は以下の感じ。

* /loginにusername, passwordをPOSTで送信し、認証OKならtoken(JWT)を発行。
* /protectedへのアクセスにはAuthorizationヘッダにBearer+tokenを付与し認証OKならコンテンツを戻す。
* 認証機構はverifyToken()という外部関数とし実装し、app.get('/protecte')のミドルウエアとして適用する。

では実装します。

```js:index.js
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" });
    }
}
```

実装できたら実行。

```bash
node index.js
```

## 動作確認

curlを利用して動作確認をしてみます。

### tokenの取得

まずは認証してtokenを取得します。

```bash
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にアクセスしてみます。


```bash
curl -X GET http://localhost:3000/protected -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJpYXQiOjE2MTE3OTEyMDksImV4cCI6MTYxMTc5NDgwOX0.Gy_3r3T-AQG8iL28LE1xzVNMEJDKTtgTyMRiaSNpQiM"

Protected Contents
```

うまく表示されました。

## その他

### リフレッシュはどうする？

調べ中。
expireの違う2つのtokenを発行し１つをrefresh tokenとして使うという実装例も。
https://medium.com/@had096705/build-authentication-with-refresh-token-using-nodejs-and-express-2b7aea567a3a