【JWT（JSON Web Token）】Node.jsで実際に使ってみた

# はじめに
認証方式の１つであるJWTについてのまとめと使用例

# JWTとは
JSON Web Tokenの略
認証情報を含むJSONをbase64エンコードしたものに署名を付与したもの

# 利用例
1. クライアント側から認証情報（例：ユーザー名、パスワード）をサーバーに送信
2. サーバー側で認証情報を確認し、認証OKの場合JWTを発行し、クライアント側に返却
3. クライアントは次回以降、JWTを付与したリクエストを送信
4. サーバー側はJWTを検証する

なお、JWTの暗号化アルゴリズムは大きく分けて2種類ある。

- 共通鍵方式
HS256というアルゴリズムを使用する。
認証サーバとリソースサーバが同じ場合はこの方式が使われることが多い。

- 公開鍵／秘密鍵方式
RS256というアルゴリズムを使用する。
認証サーバとリソースサーバが別々の場合にこの方式が使われる。
認証サーバに秘密鍵、リソースサーバに公開鍵が配置される。

# JWTの構造
JWTの例は以下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFyb3UiLCJpYXQiOjE1OTIyNDAxODF9.vgsytL2KiAp-LXFSSmVXObia0bStoZqOCdYoEXdRaz8

※[JWT公式](https://jwt.io/)に貼り付けると内容がわかる

形式は[ヘッダー].[ペイロード].[署名]となる。

### ヘッダー

```json
{
  "alg": "HS256",
  "typ": "JWT"
}
```

署名の暗号化方式とトークンの種類を設定

### ペイロード

```json
{
  "user": "tarou",
  "iat": 1592240181
}
```

実際のデータの中身
base64エンコードしているだけなので、パスワードとかの重要情報を含んではいけない。
上記の例以外にもトークンの有効期限や発行者などの情報を設定することもできる。


### 署名
ヘッダーとペイロードを鍵で暗号化した値
鍵はサーバー側で管理しておく。
署名を検証することによって、データの改ざんが行われていないかチェックすることができる。


# 実際に使ってみた

Node.js/ExpressでAPIを作ってみる。
作成するAPIは以下２つ

- JWT発行API
- 認証必須API

今回は共通鍵方式による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 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を使って動作確認。

まずはJWT発行APIです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638864/2246f4fa-36c3-a767-a72f-3622cc8d0b06.png)

トークンが返ってきました！

次にトークンの有効期限が切れないうちに認証必須APIを呼ぶ。
リクエストヘッダのauthorizationにBearerスキームでトークンを設定するのを忘れずに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638864/0734d119-d3ad-16f3-a0e9-464d6f3639ae.png)

自分の名前が返ってきました！
認証が成功した証。


次にトークンを改ざんして送ってみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638864/65e970fe-0db6-af21-4bcc-51946fecb21f.png)

ステータス500で無効なトークンとのメッセージが返ってきました！


次に有効期限切れの場合
もう1分経ったので、正しいトークンを送信しても…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638864/70d68119-03b9-746a-3049-dfc1d14c8469.png)

期限切れのメッセージ！

完璧ですね。

ちなみに今回生成されたJWT。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFyb3UiLCJpYXQiOjE1OTIzMjUxMzksImV4cCI6MTU5MjMyNTE5OX0.Uoqk6Yz129DKQCcvpSKhAw3Ncjln6ILucWAz_1ZLFhg

これを[JWT公式](https://jwt.io/)に貼り付けると以下のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638864/421335fe-34fe-fde3-23a9-6ff319ad9bdf.png)




# トークンの保存場所とサーバへの送信方法
いろんな方法があるよう
また、後述する脆弱性との兼ね合いもあり、何がベストプラクティスなのかは正直全く分かっていない。

- サーバー側でcookieに保存して、そのままやりとりする
- クライアント側はcookieもしくはweb strageに保存して、必要な場合のみリクエストヘッダに付与する
- Authorizationに設定する場合は、Bearerスキームが一般的？
- リクエストボディに入れてもいい


# 脆弱性
JWTについて調べていると、脆弱性の指摘について、いろいろな記事を見かけた。
ただ、自分の知識が足らず全てを理解することはできなかったので、以下にメモ程度として残しておく。

- cookieに保存するとCSRFの恐れがある
- web strageに保存するとXSSの恐れがある
- cookie or web strageに保存して、使う場合だけリクエストヘッダに含める
- 有効期限を過ぎるまで無効化する方法がないため、有効期限は極力短くすること
- ということは、セッション管理などでは使えなさそう
- 上記の脆弱性も様々な手法で回避できる？

# 思ったこと
JWTを**理解する**ことはそこまで難しくないし、実際に試すことも簡単だったが、
JWTを**使いこなす**には、OAuthなどの認証方式や、XSS・CSRFなどの攻撃手法などを理解する必要があり、結構ハードルが高そう。

# 追記：6/22

今回の記事で紹介したのは、共通鍵暗号化方式を使用したJWTの使い方ですが、以下の記事で公開鍵・秘密鍵暗号化方式を使用した場合のサンプルも作成しましたので、興味がある方は見てみてください。

- [【JWT（JSON Web Token）】Node.jsで実際に使ってみた（公開鍵・秘密鍵方式）](https://qiita.com/sa9ra4ma/items/7e8bb9e4877249ff2001)

# 参考
- [JWTを認証用トークンに使う時に調べたこと](https://christina04.hatenablog.com/entry/2016/06/07/123000) ※一番わかりやすかった
- [JSON Web Token（JWT）のClaimについて](https://kamichidu.github.io/post/2017/01/24-about-json-web-token/)
- [JSON Web Tokenを完全に理解する](https://qiita.com/k_k_hogetaro/items/0c97f42ecb8207767db2)