【JWT（JSON Web Token）】Node.jsで実際に使ってみた（公開鍵・秘密鍵方式）

# はじめに

以前、JWTをNode.jsで使ってみる記事（[コチラ](https://qiita.com/sa9ra4ma/items/67edf18067eb64a0bf40)）を書きましたが、これは共通鍵暗号化方式を使用したものでした。
今回は公開鍵・秘密鍵暗号化方式を使用した場合のサンプルを作成したので、紹介したいと思います。

JWTとは何かをまだ理解していない方は以下の記事から読んでみてください。

- [【JWT（JSON Web Token）】Node.jsで実際に使ってみた](https://qiita.com/sa9ra4ma/items/67edf18067eb64a0bf40)

# 実際に使ってみた
JWTの説明は以前まとめたので、いきなり使ってみようと思います。

# ソースの説明

ソース全部貼っておきます。

```javascript
// ➀
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 fs = require('fs')
const PRIVATE_KEY = fs.readFileSync('secret.pem');      // 秘密鍵
const PUBLIC_KEY = fs.readFileSync('secret.pem.pub');   // 公開鍵

// ➂JWT発行API
app.post('/login', (req, res) => {
    // 動作確認用に全ユーザーログインOK
    const payload = {
        user: req.body.user
    };
    const option = {
        algorithm: 'RS256',                             // 公開鍵・秘密鍵暗号化方式を指定
        expiresIn: '1m'
    }
    var token = jwt.sign(payload, PRIVATE_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');
    }

    const option = {
        algorithms: 'RS256'                             // 検証時も暗号化方式を指定
    }
    
    // トークンの検証
    jwt.verify(token, PUBLIC_KEY, option, 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));
```

共通鍵方式から変更した部分のみ、説明します。

- ➁鍵
今回は秘密鍵と公開鍵をファイルに保存し、それぞれ読み込んでいます。

- ➂JWT発行API
JWT発行時のオプションに`algorithm: 'RS256'`を追加しています。
JWT発行時の鍵に秘密鍵を使用しています。

- ➃認証用ミドルウェア
JWT検証時のオプションにも`algorithm: 'RS256'`を追加しています。
JWT検証時の鍵に公開鍵を使用しています。


以上です！

動作確認は共通鍵方式と同じなので、今回は省略します。

# 詰まったところ

完全に自分の知識不足なのですが、公開鍵・秘密鍵にも複数の種類があることを知りませんでした。
JWTは(jsonwebtokenモジュールは？)PEM形式しか受け付けません。

自分は最初にtera termを使って鍵を生成したのですが、それはOpenSSH形式だったようで、JWT発行時に以下のようなエラーレスポンスが返ってきました。

```json
{
    "library": "PEM routines",
    "function": "get_name",
    "reason": "no start line",
    "code": "ERR_OSSL_PEM_NO_START_LINE"
}
```

結局、以下のコマンドで鍵を生成しました。
`-m PEM`でPEM形式で鍵を生成できます。

```shell
ssh-keygen -t rsa -b 4096 -m PEM
```

これでJWT発行APIは正常に通り、トークンが返されました。

そして、トークンを設定して、認証必須APIを呼ぶと

```
error:0909006C:PEM routines:get_name:no start line
```

エラーです…
また、PEM…？

なんと、先ほど鍵を生成したコマンドでは、秘密鍵はPEM形式になるのに公開鍵はPEM形式にならないようです。
明示的にPEM形式に変換してやる必要があります。

```shell
ssh-keygen -f secret.pem.pub -e -m pem > secret.pem.pub_
mv secret.pem.pub_ secret.pem.pub
```

これでようやく公開鍵・秘密鍵ともにPEM形式となり、無事に認証を実現することができました。


# おわりに
前回は、「OAuthやXSS、CSRFなどの知識がないと難しい」と書いたが、今回は、「公開鍵の形式の違いが分からないと難しい」となりそうです。

世の中の技術は単体で成立するものは少なく、ほとんどのものが他の技術と紐づいて成立しているのだとつくづく感じさせられます。

今は必要ないと思っている知識も突然必要になったり、新しい技術の理解のために必要な技術の理解のために必要な技術の…という沼にハマったりというのは皆さんあるあるだと思います。

いざという時、少しでも時間のロスを減らすためには常日頃から少しでも新しい技術に触れることは重要ですね。
実際に体験しなくてもドキュメントや記事を読んで技術の存在を知るだけでもだいぶ違うと思います。


# 参考

[RSA鍵のフォーマットを作って見て変換して体験実習しみた](https://qiita.com/ponsuke0531/items/7b34347213660c80bc95)
[【 ssh-keygen 】コマンド――SSHの公開鍵と秘密鍵を作成する](https://www.atmarkit.co.jp/ait/articles/1908/02/news015.html)