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


はじめに

認証方式の1つである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公式に貼り付けると内容がわかる

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

ヘッダー

{
  "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を使って動作確認。

まずはJWT発行APIです。

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

次にトークンの有効期限が切れないうちに認証必須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の使い方ですが、以下の記事で公開鍵・秘密鍵暗号化方式を使用した場合のサンプルも作成しましたので、興味がある方は見てみてください。

参考