68
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-17

はじめに

認証方式の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です。
image.png

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

次にトークンの有効期限が切れないうちに認証必須APIを呼ぶ。
リクエストヘッダのauthorizationにBearerスキームでトークンを設定するのを忘れずに
image.png

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

次にトークンを改ざんして送ってみる。
image.png

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

次に有効期限切れの場合
もう1分経ったので、正しいトークンを送信しても…
image.png

期限切れのメッセージ!

完璧ですね。

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

これをJWT公式に貼り付けると以下のようになる。
image.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の使い方ですが、以下の記事で公開鍵・秘密鍵暗号化方式を使用した場合のサンプルも作成しましたので、興味がある方は見てみてください。

参考

68
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?