本文書ではExpress.jsを使ったJWT認証サーバの構築方法の説明を行っています。ユーザ情報を保存するデータベースではSQLiteとMongoDBを利用しています。好きなデータベースを選択して読み進めてください。Expressサーバに送信するGETリクエストやPOSTリクエストはVisual Studio CodeのRest Clientを利用しています。

本文書ではJWTを利用してTOKENを作成しクライアントに送信するバックエンド側の設定を行なっています。フロントエンド側にはVue.jsのフレームワークNuxt.jsを使うことを前提に記述しているため下記の文書をあわせて読むとNuxt.jsでのユーザ認証機能の実装方法の基礎を理解することができます。

本文書で実装する機能は下記の通りです。

  • URL:/api/auth/registerにユーザ登録のPOSTリクエストがあった場合、データベースSQLiteまたはMongoDBのusersテーブル(or usersコレクション)にユーザ情報を保存する
  • URL:/api/auth/loginにPOSTリクエストがあった場合、送られてくるユーザ情報のチェックを行い、登録済みのユーザである場合はTOKEN情報を返す
  • URL:/api/auth/userにGETリクエストがあった場合ヘッダーに保存されているTOKENの確認を行い、問題がなれればユーザ情報を戻す
公開当初はSQLiteのみの設定方法を記述していましたが後日MongoDBでの設定方法を追記しています。

JWTって難しいの?

本文書を読んでいる人の中にはJWT(JSON WEB TOKEN)は認証に関わっているため難しいのではないかという不安を持っている人もいるかと思いますがこの文書で記述しているJWTに関する処理はユーザ名とパスワードを使ってTokenを作成するだけです。Tokenの作成はたった1行で終わります。残りはExpressサーバの設定、データベースへのユーザ情報の保存、POSTリクエストの中身の取得と作成したTokenをクライアントに戻す処理です。これらの処理はTokenに特化したことではないのでリクエストの処理に関するコードの記述経験があれば全く難しい箇所はありません。

別の文書での説明になりますがフロントエンドの処理についてもログインが完了したらTokenを受け取って受け取ったTokenを送信してサーバ側で検証が行われ問題がなければユーザ情報が戻されその情報をすべてのコンポーネントでアクセスできるようにVueならVuex, ReactならReduxまたはContextに保存するだけです。ユーザ情報の保存とは別に一度取得したTokenをlocalStorageに保存するのかCookieに保存するのかまたは保存しないのかということを決めることになりますが仕組みはシンプルです。

環境設定

Express.jsのインストール

Express.jsインストール用のディレクトリbackendを作成します。ディレクトリ名は任意なので適切な名前をつけてください。作成後、backendディレクトリに移動します。


 $ mkdir backend
 $ cd backend

パッケージ管理を行うためにpackage.jsonファイルを作成します。ファイルの作成はnpm init -yコマンドで行います。


 $ npm init -y
npm initコマンドでは対話式でインストールを行いますが、-yオプションをつけると対話式をスキップしてデフォルト設定でpackage.jsonファイルが作成されます。

package.jsonファイル作成後、npm installコマンドを使ってexpressパッケージのインストールを行います。


 $ npm install express

Express.jsの動作確認

Express.jsのインストールが正常に完了しているか確認するために動作確認を行います。

backendディレクトリにindex.jsファイルを作成して下記のコードを記述します。ポートを5000に設定し、”/”(ルート)にアクセスがあるとHello Worldを戻すだけのシンプルなコードです。


const express = require('express')
const app = express()
const port = 5000

app.get('/', (request, response) => response.send('Hello World!!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

index.jsファイルにコードを記述後は下記のコマンドを実行してください。


 $ node index.js 
Example app listening on port 5000!

ブラウザからhttp://localhost:5000にアクセスするとブラウザ上にHello World!!が表示されていることを確認してください。表示されればExpress.jsの動作確認を完了です。

nodemonのインストール

Express.jsのコードを記述するindex.jsファイルのファイル更新を監視するためにnodemonパッケージをインストールします。index.jsファイルの更新を行うとnodemonが自動で更新を反映してくれます。必須ではありませんが効率的に動作確認を行うことができます。


 $ npm install nodemon

先程は動作確認のためnode index.jsコマンドでExpressサーバを起動しましたがnodemonインストール後は以下のコマンドを実行しておきます。nodemonを利用したコマンドの実行することでindex.jsファイルを更新するとnodemonが更新を監視し自動でファイルの再読み込みを行ってくれるようになります。


$ npx nodemon index.js
[nodemon] 2.0.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Example app listening on port 5000!

SQLiteを利用した場合

mongoDBを利用したい場合は本章をスキップしてください。

動作確認を行う環境にSQLiteのインストールが行われていない場合は先にSQLiteのインストールを行ってください。macOSではデフォルトからインストールされているためインストールをする必要はありません。

SQLite用のパッケージをインストール

※SQLiteではなくMongoDBを利用した設定方法については後半に記載しています。

Sqlite3データベースをExpress.jsから利用するためのパッケージをインストールします。


 $ npm install sqlite3

SQLiteのデータベースの作成

SQLiteのデータベースの作成を行います。backendディレクトリで実行します。


 $ mkdir database
 $ touch database/database.sqlite3

Express.jsから作成したデータベースに接続できるかの確認を行います。


const express = require('express')
const app = express()
const port = 5000

const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database('./database/database.sqlite3', (err) => {
    if (err) {
      return console.error(err.message);
    }
    console.log('Connected to the SQlite database.');
  });

app.get('/', (request, response) => response.send('Hello World!!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

nodemonを実行しているコンソールにはデータベースへの接続が完了したメッセージが表示されます。


[nodemon] restarting due to changes...
[nodemon] starting `node index.js`
Example app listening on port 5000!
Connected to the SQlite database.

データベースの接続が完了できたのでusersテーブルの作成を行います。nodemonで監視している状態で下記のコードをindex.jsファイルの下部に追加します。保存した瞬間にnodemonが変更を検知してテーブル作成のコードを実行します。ターミナルにtable createdが表示されたらテーブルの作成は完了です。テーブルの作成後はindex.jsファイルからテーブル作成のコードは削除してください。


let sql = `CREATE TABLE USERS(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    email TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL
    )`

db.run(sql,(err) => {
    if(err) {
        return console.error(err.message);
    }
        console.log('table created');
    }
 )
テーブルが作成済みの場合はtable USERS already exitestのエラーが表示されます。

SQLiteデータベースの中身を確認するためにデータベース管理ソフトウェアのTablePlusを利用します。

接続が完了するとusersテーブルが作成されていることを確認することができます。

テーブルの作成
テーブルの作成
usersテーブルの作成はコードからではなくTablePlusから直接行うこともできます。

bcryptパッケージのインストール

データベースに保存するユーザのパスワードはハッシュ化して保存するためにbcryptパッケージのインストールを行います。


 % npm install bcrypt

インストール後、index.jsファイル内でbcryptをrequireで読み込みsaltRoundsの設定を行います。bcryptではsaltRoundsを使って何回ハッシュ化を行うのか設定を行います。10はnの10乗を意味するので1024回のハッシュ化を行います。


const bcrypt = require('bcrypt')
const saltRounds = 10

ユーザの登録

作成したデータベースにユーザの登録が行えるのかどうか動作確認を行います。

下記のユーザ作成のコードを一時的にdb接続のコードの下に記述してください。ユーザ名はjohndoe, メールアドレスはjohn@example.com、パスワードはpasswordに設定を行っています。


const insert = 'INSERT INTO USERS (name, email, password) VALUES (?,?,?)'

bcrypt.hash("password", saltRounds, (err, hash) => {
    db.run(insert, ["johndoe","john@example.com",hash])
})

TablePlusで確認するとユーザが登録されていることが確認できます。またパスワードもハッシュ化されていることも確認できます。bcryptが問題なく動作していることも確認できました。

テーブルへのユーザ登録
テーブルへのユーザ登録
ユーザ作成を確認した後はユーザ作成のコードはindex.jsファイルから削除しておいてください。

作成したユーザ一覧情報の取得

エディターにVisual Studio Codeを利用している場合は、拡張機能REST clientをインストールすると簡単にHTTPリクエストでExpressサーバにアクセスすることが可能です。

REST clientのインストール
REST clientのインストール
エディターにVisual Studio Codeを利用していない人はPostmanなどの他のツールで動作確認を行ってください。

インストールが完了したら拡張子に.restか.httpをつけて任意の名前のファイルを作成してください。ここではtest.httpという名前でファイルを作成しています。

test.httpファイルを作成します。最初にExpressサーバにアクセスできるか確認を行います。


GET http://localhost:5000/

test.httpに上記のGETの一行を追加すると上部にSend Requestと表示されるのでクリックするとHTTPのGETリクエストが行われます。画面の右側にサーバからのレスポンスが表示されます。左側の画面にHello World!!が表示されたらHTTPのGETリクエストからのレスポンスが正常に戻されていることがわかります。REST Clientを利用すると簡単にHTTPリエクストが送信できることが理解できたかと思います。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 13
ETag: W/"d-pqfIFYs01VSVSkySGxRPgtddtoM"
Date: Thu, 04 Nov 2021 04:29:05 GMT
Connection: close

Hello World!!

次にusersテーブルに保存されているユーザ情報の一覧を取得できるようにindex.jsにルーティングを追加します。SQLiteデータベースにアクセスしてselect * from usersでユーザ情報を取得しています。sqlの処理にエラーが発生した場合はステータスコード400(Bad Request)とエラーメッセージを戻すように設定しています。


app.get("/api/users", (req, res) => {
    const sql = "select * from users"
    const params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
          return res.status(400).json({"error":err.message})
        }
        return res.json({
            "message":"success",
            "data":rows
        })
      });
});

test.httpにGETリクエストを追加してユーザ一覧を取得します。


GET http://localhost:5000/api/users

登録しているユーザが一人の場合は下記のように表示されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 157
ETag: W/"9d-caqalxmvX0ZjV7FNQ0LU2j4j2Dk"
Date: Thu, 04 Nov 2021 04:31:24 GMT
Connection: close

{
  "message": "success",
  "data": [
    {
      "id": 1,
      "name": "johndoe",
      "email": "john@example.com",
      "password": "$2b$10$oiH..aBetwiSQYuJtck8YuupafgODNS53jQ8bcs3i.7CMw4Tc3JQ."
    }
  ]
}

エラーの場合の動作確認も行っておきます。index.jsファイル内のテーブルの名前をusersからuserに変更した場合はsqlコマンドでエラーが発生してステータスコード400とエラーメッセージが戻されtest.httpファイルの右側の画面には下記のメッセージが表示されます。


HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 45
ETag: W/"2d-UO1+fW2HQ3gbb8Zjpc1L/WYjr7U"
Date: Thu, 04 Nov 2021 04:33:30 GMT
Connection: close

{
  "error": "SQLITE_ERROR: no such table: user"
}

POSTリクエストによるユーザ登録

HTTPのPOSTリクエストでユーザ登録ができるようにコードの追加を行います。

フロントエンドにユーザ登録フォームを作成し、入力した情報をPOSTリクエストで送信することでユーザの作成を行うことができます。

ユーザを登録するルーティングを追加します。先程のユーザ登録では直接ユーザ名、パスワード、メールアドレスを追加していましたが、今回はPOSTリクエストでそれらの情報を受け取る必要があります。


app.post('/api/auth/register/', (req, res) => {
  const insert = 'INSERT INTO USERS (name, email, password) VALUES (?,?,?)'
  bcrypt.hash(req.body.password, saltRounds, (err, hash) => {
    db.run(insert, [req.body.name,req.body.email,hash],(err) => {
      if (err) {
        return res.status(400).json({"error":err.message});
      }
      return res.json({
        "message": "create User successfully",
        "data": [req.body.name, req.body.email]
      })
    })
  })
})

test.httpにPOSTリクエストを記述しユーザの登録ができるか確認を行います。


POST http://127.0.0.1:5000/api/auth/register/
Content-Type: application/json

{
    "name": "kevin",
    "email": "kevin@test.com",
    "password": "password"
}
Content-Typeの下の空行やPOSTリクエストの下に空行が入る場合はリクエストがうまく動作しないので空白にも注意してください。

実行するとInternal Server Error 500が発生して以下のメッセージが表示されます。これは受け取ったPOSTリクエストの内容をExpressサーバ側で取り出すことができないためです。


TypeError: Cannot read property 'password' of undefined

POSTリクエストから送られてきた内容を取り出すために下記を追加します。


app.use(express.json())

追加後、再度POSTリクエストを送ると登録したユーザのnameとemailが戻されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 72
ETag: W/"48-H9j2SANHo+FmucjuENhYuCWuLqM"
Date: Thu, 04 Nov 2021 04:37:11 GMT
Connection: close

{
  "message": "create User successfully",
  "data": [
    "kevin",
    "kevin@test.com"
  ]
}

再度同じメールアドレスでユーザを登録しようとするとエラーメッセージが表示されます。


HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 68
ETag: W/"44-ffAuTeqwiyGjM74XB3CH5FnCVcE"
Date: Thu, 04 Nov 2021 04:37:31 GMT
Connection: close

{
  "error": "SQLITE_CONSTRAINT: UNIQUE constraint failed: USERS.email"
}

ユーザのパスワードチェック

登録したユーザのメールアドレスとパスワードを使ってPOSTリクエストを送ると送ったユーザ情報とデーターベースに保存されているユーザ情報が一致するかチェックする機能を追加します。

ここでの設定はフロントエンド側のログインフォームから送られてくるメールアドレスとパスワードを使った認証を行う部分で利用します。

POSTリクエストに含まれるメールアドレスがusersテーブルに存在しているのか確認を行い、存在する場合はユーザ情報を取り出し(user)、取り出したパスワードとPOSTリクエストで送られてきたパスワードが一致するか確認しています。

パスワードのチェックには、bcrypt.compareを利用しています。bcrypt.compareではPOSTリクエストに含まれるパスワードとデータベースから取り出したパスワードを比較しています。データベースから取り出したパスワードがハッシュ化されているのでbcrypt.compareが必要になります。


app.post('/api/auth/login/',(req,res) => {
  const sql = 'select * from users where email = ?'
  const params = [req.body.email]
  db.get(sql, params, (err, user) => {
    if (err) {
      return res.status(400).json({"error":err.message});
    }
    if(!user){
      return res.json({"message": "email not found"})
    }
    bcrypt.compare(req.body.password, user.password, (err,result) => {
      if (err) {
        return res.status(400).json({"error":err.message});
      }
      if (!result) {
        return res.json({"message" : "password is not correct"})
      }
      return res.json({"message" : "password is correct"})
    })
  })
})

test.httpを使ってPOSTリクエストを行い正しいメールアドレスとパスワードを送信した場合のみ”message”: “password is correct”が表示されることを確認してください。test.httpファイル内で複数のリクエストを記述する場合は区切り文字として###をリクエストの上に追加します。


###
POST http://127.0.0.1:5000/api/auth/login/
Content-Type: application/json

{
    "email": "kevin@testcom",
    "password": "passwor"
}

ここまででユーザの登録とメールアドレスをパスワードのチェックを行う機能(ユーザ認証)の追加が完了しました。

JSON WEB TOKENのインストール

ユーザ登録後、ログインに成功した場合のみTokenを渡します。Tokenの処理はjsonwebtokenパッケージを利用します。

npm installコマンドでjsonwebtokenのインストールを行います。


 $ npm install jsonwebtoken

インストールしたjsonwebtokenをrequireで読み込みます。


const jwt = require('jsonwebtoken')

JWTのTokenの作成

メールアドレスとパスワードによる認証が正常に完了した場合のみTokenを渡すため、index.jsのパスワード認証のresponseを更新する必要があります。

JWTを利用してTokenを作成するためには最低payloadとsecretキー(秘密鍵)が必要になります。payloadにはTokenに含めたい情報を設定します。payloadはキーと値をペアに持つオブジェクトです。ペイロードではどのようなキーと値の設定を行っても問題ありませんがTokenは暗号化されないため他の人が見ても大丈夫な情報のみ設定してください。jwt.signの2番目の引数にはハッシュ化に利用するsecretキーを設定します。このsecretキーの値は任意です。ここでは”secret”を設定しています。


// return res.json({"message" : "password is correct"})

      const payload = {
                        id: user.id,
                        name: user.name,
                        email: user.email
                      }

      const token = jwt.sign(payload,'secret')
      return res.json({token})

TokenはHeader(ヘッダー), Payload(ペイロード), Signature(署名)で構成されます。Headerには署名の作成で利用するアルゴリズムの情報が含まれています。Payloadには自由に設定ができるオブジェクトがキーと値のペアで含まれています。最後のSignatureはHeaderとPayloadをsecretキーとHeaderに設定されているアルゴリズムを使って作成されます。SignatureはPaylodとsecretキー(秘密鍵)を持っているものだけが作成することができます。

Tokenを受け取ったユーザはAuthorization Headerに受け取ったTokenを入れてサーバに送信します。受け取ったサーバはTokenとsecretキーを利用してTokenに含まれる署名が正しいものであるかチェックを行います。secretキーを持っているものしかTokenが正しいかどうかのチェックを行うことはできません。

デフォルトでは署名に利用するアルゴリズムはHMAC SHA256に設定されています。設定を変更したい場合はsingメソッドの中で下記のように変更を行うことができます。


jwt.sign(payload, 'secret, { algorithm: 'RS256' })

Tokenに有効期限を設定することもでき有効期限はpayloadの中に含めます。下記は1時間を設定した場合です。


jwt.sign({
  exp: Math.floor(Date.now() / 1000) + (60 * 60),
  id: user.id,
  ...
}, 'secret');

Tokenの作成のコードを追加するとtest.httpを利用して正しい認証情報のメールアドレスとパスワードを使ってPOSTリクエストを行うとTokenを入手することができます。


###
POST http://127.0.0.1:5000/api/auth/login/
Content-Type: application/json

{
    "email": "kevin@testcom",
    "password": "passwor"
}

REST ClientでPOSTリクエストを送信するとTokenが戻されます。リクエスト毎にTokenの値が変わることも確認しておいてください。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 180
ETag: W/"b4-L7hWXlLxAbI2qGOg8DbcxGnFL2k"
Date: Thu, 04 Nov 2021 04:53:55 GMT
Connection: close

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwibmFtZSI6ImtldmluIiwiZW1haWwiOiJrZXZpbkB0ZXN0LmNvbSIsImlhdCI6MTYzNjAwMTYzNX0.aZ7VvynD9RLX_Usklbxu3KTM8zPzgkPADBWuVLgLc-o"
}

Tokenに含まれるpayloadは先ほど説明した通り暗号化されているわけではないので、jwt.ioのdebuggerを利用するとpayloadの中身を確認することができます。Tokenの値が分かれば誰でもpayloadの中身を確認することできます。

JWT TOKENの確認
JWT TOKENの確認

Tokenの確認

Tokenをブラウザに渡した後、ブラウザが再度アクセスしてきた場合にブラウザから送られてくるTokenが正しいのかチェックを行う必要があります。

新たに/api/auth/userのルーティングを追加します。下記の処理ではGETリクエストのヘッダーに入っているauthorizationからTokenを取り出しています。Tokenはauthorizationの中でBearer XXXX(Token)の形式で入っています。

jwt.verifyには取り出したTokenとToken作成時に利用したキー”secret”を利用して確認を行っています。正しい場合はTokenに含まれていたpayloadを戻しています。


app.get('/api/auth/user/',(req,res) => {

  const bearToken = req.headers['authorization']
  const bearer = bearToken.split(' ')
  const token = bearer[1]

  jwt.verify(token,'secret',(err,user)=>{
    if(err){
      return res.sendStatus(403)
    }else{
      return res.json({
            user
          });
    }
  })
});

http://127.0.0.1:5000/api/auth/login/へのPOSTリクエストで取得したTokenをAuthorizationに設定してTokenの認証を行います。


###
GET http://127.0.0.1:5000/api/auth/user/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibmFtZSI6ImtldmluIiwiZW1haWwiOiJrZXZpbkB0ZXN0LmNvbSIsImlhdCI6MTU3NzE1NDA4Nn0.uSSn6Q8IGK1xL2EcsxX3dX_3XZIuuh8lZ4UxzShZg34

Tokenが正しい場合は、以下のようにpayloadに設定したユーザの情報を取得することができます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 74
ETag: W/"4a-XqOaLbPScM2AUL+fsEIJm/XBS1k"
Date: Thu, 04 Nov 2021 05:01:37 GMT
Connection: close

{
  "user": {
    "id": 5,
    "name": "kevin",
    "email": "kevin@test.com",
    "iat": 1577154086
  }
}

もしTokenに誤りがある場合は下記のようにForbiddenが表示されます。


HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 9
ETag: W/"9-PatfYBLj4Um1qTm5zrukoLhNyPU"
Date: Tue, 24 Dec 2019 02:41:07 GMT
Connection: close

Forbidden

ここではデータベースのSQLiteを利用していましたがユーザ情報を保存することができればSQLiteである必要はありません。

実際に利用する場合はセキュリティについて考える必要がありますが、本文書を通してExpress.jsにおけるJWTの設定方法の流れを理解することができたと思います。

本文書ではフロントエンド側について全く触れていないので、フロントエンドを含めて全体の設定を理解したいと思います。フロントエンドについてはNuxt.jsを利用して動作確認を行っているので下記の文書を参考にしてください。

MongoDBを利用した場合

SQLiteではなくMongoDBを利用した場合の設定方法について説明を行います。

MongoDBはクラウドベースのMongoDB Atlasを利用します。アカウントの作成からExpressサーバからクラウドのMongoDBの説明まで以下の文書で解説しているので参考にしてください。

mongooseのインストール

MongoDBへはmongooseを利用して接続を行うのでmongooseパッケージのインストールを行います。


 % npm install mongoose

MongoDBへのアクセス

MongoDBの管理画面で取得できる接続情報を元にMongoDBへの接続を行います。下記の接続情報では接続できないので各自が取得した情報で設定を行ってください。


const express = require('express');
const app = express();
const mongoose = require('mongoose');

const port = 5000;

const options = {
  useUnifiedTopology: true,
  useNewUrlParser: true,
};

mongoose.connect(
  'mongodb+srv://mongodb:<YOUR PASSWOR>@cluster0.cccc.mongodb.net/myFirstDatabase?retryWrites=true&w=majority',
  options
);

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'DB connection error:'));
db.once('open', () => console.log('DB connection successful'));
データベースの名前は接続情報に含まれるmyFirstDatabaseとなります。任意の名前をつけることは可能ですが、本書ではデフォルトのmyFirstDatabaseをそのまま利用します。

nodemonが更新を検知し、メッセージに”DB connection successful”が表示されればMongoDBへの接続は正常に行われています。


[nodemon] starting `node index.js`
DB connection successful

Modelの設定

mongooseを利用しているのでModelを介してMongoDBのコレクション、ドキュメントの追加、更新、削除を行うためスキーマを設定する必要があります。modelsフォルダを作成してその中にUser.jsファイルを作成してください。

MongoDBではSQLiteやMySQLのようにテーブルや行という単語は利用しません。その代わりテーブルに対応する言葉はコレクション、行に対応する言葉はドキュメントとなります。

スキーマはname, email, passwordから構成されます。typeには文字列のString, すべての値は必須なのでrequiredをtrueに設定しています。


const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    require: true,
    unique: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
});

module.exports = mongoose.model('User', UserSchema);

bcryptパッケージのインストール

データベースに保存するユーザのパスワードはハッシュ化して保存するためにbcryptパッケージのインストールを行います。


% npm install bcrypt

インストール後、index.jsファイル内でbcryptをrequireで読み込みsaltRoundsの設定を行います。bcryptではsaltRoundsを使って何回ハッシュ化を行うのか設定を行います。10はnの10乗を意味するので1024回のハッシュ化を行います。


const saltRounds = 10;
const hashedPassword = await bcrypt.hash('password', saltRounds);

ユーザ登録の動作確認

ここまでの設定でユーザの登録が行えるか確認を行います。ルーティングの/(ルート)にブラウザからアクセスするとユーザ名:John, メールアドレスjohn@example.com、パスワードpasswordでユーザが作成されるように設定しています。


const db = mongoose.connection;

const User = require('./models/User');

const bcrypt = require('bcrypt');
const saltRounds = 10;

app.get('/', async (req, res) => {
  try {
    const hashedPassword = await bcrypt.hash('password', saltRounds);

    const newUser = await new User({
      name: 'John',
      email: 'john@example.com',
      password: hashedPassword,
    });

    await newUser.save();
    res.send('hello world');
  } catch (err) {
    console.log(err);
  }
});

ブラウザでアクセス後にMongoDBの管理画面にアクセスしてユーザが作成されていることを確認してください。passwordはbcryptでハッシュ化されていることも確認できます。

mongoDBにユーザが作成されていることを確認
mongoDBにユーザが作成されていることを確認

作成したユーザ情報の取得

エディターにVisual Studio Codeを利用している場合は、拡張機能REST clientをインストールすると簡単にHTTPリクエストでExpressサーバにアクセスすることが可能です。

REST clientのインストール
REST clientのインストール
エディターにVisual Studio Codeを利用していない人はPostmanなどの他のツールで動作確認を行ってください。

インストールが完了したら拡張子はrestかhttpで任意のファイルを作成してください。ここではtest.httpという名前でファイルを作成しています。

test.httpファイルを作成します。最初にExpressサーバにアクセスできるか確認を行うため先ほどユーザ作成のために追加したコードをコメントして/(ルート)ヘのアクセスがあったら”hello world”を返すように設定を行います。


app.get('/', async (req, res) => {
  res.send('hello world')
  // try {
  //   const hashedPassword = await bcrypt.hash('password', saltRounds);

  //   const newUser = await new User({
  //     name: 'John',
  //     email: 'john@example.com',
  //     password: hashedPassword,
  //   });

  //   await newUser.save();
  //   res.send('hello world');
  // } catch (err) {
  //   console.log(err);
  // }
});

REST Clientの動作確認を行うため作成したtest.httpファイルを開いてGET http://localhost:5000を入力するとSend Requestが表示されるのでSend Rquestをクリックしてください。

REST Clientによる接続の確認
REST Clientによる接続の確認

上記の右側の画面のようにhello worldが表示されればREST ClientからExpressサーバへの接続は正常に行われています。

次にusersコレクションに保存されているユーザ情報の一覧を取得できるようにルーティングを追加します。


app.get('/api/users', async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (err) {
    console.log(err);
  }
});

追加後REST Clientで/api/usersにGETリクエストを送信します。


GET http://localhost:5000/api/users

先ほど作成したユーザが表示されることが確認できます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 159
ETag: W/"9f-D4AbwXDy5fyxBLeDHh4huP3yc/k"
Date: Wed, 30 Jun 2021 13:37:54 GMT
Connection: close

[
  {
    "_id": "60dc6ec66f2bd63257dc4960",
    "name": "John",
    "email": "john@example.com",
    "password": "$2b$10$YIndJx0hwLB1UZ5hsUfoU.mNbywX/auMRw6I.6RptWldDI.vQ5RrO",
    "__v": 0
  }
]

POSTリクエストによるユーザの登録

HTTPのPOSTリクエストでユーザ登録ができるようにコードの追加を行います。

ユーザを登録するルーティングを追加します。先程のユーザ登録では直接ユーザ名、パスワード、メールアドレスを追加していましたが、今回はPOSTリクエストでそれらの情報を受け取る必要があります。

POSTリクエストはJSONで送信するためindex.jsに下記を追加します。


app.use(express.json())

POSTリクエストに含まれるユーザ情報はreq.bodyの中に入っているので下記のように取り出してユーザ情報として設定します。


app.post('/api/auth/register', async (req, res) => {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, saltRounds);

    const newUser = await new User({
      name: req.body.name,
      email: req.body.email,
      password: hashedPassword,
    });

    const savedUser = await newUser.save();

    res.json(savedUser)

  } catch (err) {
    console.log(err);
  }
});

test.httpファイルにPOSTリクエストを記述します。GETリクエストも後ほど利用するために###(シャープ3つ)を入力してPOSTリクエストを記述してください。POSTリクエストはJSON形式で送信します。


###
POST http://localhost:5000/api/auth/register/
Content-Type: application/json

{
    "name": "kevin",
    "email": "kevin@test.com",
    "password": "password"
}

POSTリクエストを設定するとSend Requestが表示されると思うのでクリックしてください。送信したユーザ情報が含まれる情報が戻ってきたらPOSTリクエストは成功しています。

POSTリクエスト送信
POSTリクエスト送信

再度GETリクエストを/api/usersに送信し2名のユーザ情報が表示されることも確認してください。

ユーザ認証の確認

登録したユーザのメールアドレスとパスワードを使ってPOSTリクエストを送ると送ったユーザ情報とデーターベースに保存されているユーザ情報が一致するかチェックする機能を追加します。

POSTリクエストに含まれるメールアドレスがusersコレクションに存在しているのか確認を行い、存在する場合はユーザ情報を取り出し(user)、取り出したパスワードとPOSTリクエストで送られてきたパスワードが一致するか確認しています。

パスワードのチェックには、bcrypt.compareを利用しています。bcrypt.compareではPOSTリクエストに含まれるパスワードとデータベースから取り出したパスワードを比較しています。データベースから取り出したパスワードがハッシュ化されているのでbcrypt.compareが必要になります。


app.post('/api/auth/login', async (req, res) => {
  try {
    const user = await User.findOne({ email: req.body.email });
    if (!user) return res.json({ message: 'user not found' });

    const match = await bcrypt.compare(
      req.body.password,
      user.password
    );
    if (!match) return res.json({ message: 'password not correct' });

    res.json(user);
  } catch (err) {
    console.log(err);
  }
});

メールアドレスとパスワードが一致する場合のみ認証が確認できたユーザ情報が戻されます。正しい情報を設定してPOSTリクエストを追加したルーティングの/api/auth/loginに対して行います。


###
POST http://localhost:5000/api/auth/login/
Content-Type: application/json

{
    "email": "kevin@test.com",
    "password": "password"
}

問題がない場合は下記のユーザ情報が戻ってきます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 156
ETag: W/"9c-iCcOzcOC063s+XBnSXTd97paZts"
Date: Wed, 30 Jun 2021 14:14:43 GMT
Connection: close

{
  "_id": "60dc760235248732e61697cb",
  "name": "kevin",
  "email": "kevin@test.com",
  "password": "$2b$10$B.m6qx3s35KK755.WP3X9OK1WoX.JO4ZPEQ4rjJsTt/NRCNBQxIi.",
  "__v": 0
}

メールアドレスが間違えた場合は設定したメールアドレスを持ったユーザが存在しないのでuser not foundが表示されます。


{
  "message": "user not found"
}

パスワードが間違っている場合は”password not correct”が表示されます。


{
  "message": "password not correct"
}

ここまでmongoDBを利用した場合のユーザの登録とメールアドレスをパスワードのチェックを行う機能(ユーザ認証)の追加が完了しました。

JSON WEB TOKENのインストール

ユーザが認証情報を利用してログインが完了したらEXpressサーバからTokenが戻されます。そのTokenをJWT(JSON WEB TOKEN)を利用して作成します。

JWTパッケージのインストールを行います。


% npm install jsonwebtoken

インストールが完了したらinde.jsファイルでインストールしたJWTを読み込みます。


const jwt = require('jsonwebtoken')

JWTについて

TokenはHeader(ヘッダー), Payload(ペイロード), Signature(署名)で構成されます。Headerには署名の作成で利用するアルゴリズムの情報が含まれています。Payloadには自由に設定ができるオブジェクトがキーと値のペアで含まれています。最後のSignatureはHeaderとPayloadをsecretキーとHeaderに設定されているアルゴリズムで作成されます。SignatureはPaylodとsecretキー(秘密鍵)を持っているものだけが作成することができます。

Tokenを受け取ったユーザはAuthorization Headerに受け取ったTokenを入れてサーバに送信します。受け取ったサーバはTokenとsecretキーを利用してTokenに含まれる署名が正しいものであるかチェックを行います。secretキーを持っているものしか正しいかどうかのチェックを行うことはできません。

Tokenの作成

先ほどの設定ではログインが正常に完了するとログインしたユーザの情報を戻していましたがJWTを利用する場合は Tokenを戻すためにコードの更新が必要となります。


app.post('/api/auth/login', async (req, res) => {
  try {
    const user = await User.findOne({ email: req.body.email });
    if (!user) res.json({ message: 'user not found' });

    const match = await bcrypt.compare(req.body.password, user.password);
    if (!match) res.json({ message: 'password not correct' });

    const payload = {
      id: user._id,
      name: user.name,
      email: user.email,
    };

    const token = jwt.sign(payload, 'secret');

    res.json({ token });
  } catch (err) {
    console.log(err);
  }
});

test.httpからPOSTリクエストを利用してログインを行います。


###
POST http://localhost:5000/api/auth/login/
Content-Type: application/json

{
    "email": "kevin@test.com",
    "password": "password"
}

Token作成のコードを追加したので認証情報が正しい場合はTokenが戻されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 213
ETag: W/"d5-96AcBuPCY7uyKCo+AG0Ti8ThiYE"
Date: Thu, 01 Jul 2021 01:29:23 GMT
Connection: close

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZGM3NjAyMzUyNDg3MzJlNjE2OTdjYiIsIm5hbWUiOiJrZXZpbiIsImVtYWlsIjoia2V2aW5AdGVzdC5jb20iLCJpYXQiOjE2MjUxMDI5NjN9.L-JbbZ-vb_sFglo6oouenpwX3yyC2Qyb3qW__RP7X9c"
}

Tokenの確認

サーバからTokenを受け取ったらそのTokenを使ってサーバにアクセスすることになります。サーバ側では送信されてくるTokenが正しいかチェックを行う必要があります。

新たに/api/auth/userのルーティングを追加します。下記の処理ではGETリクエストのヘッダーに入っているauthorizationからTokenを取り出しています。Tokenはauthorizationの中でBearer XXXX(Token)の形式で入っています。

jwt.verifyには取り出したTokenとToken作成時に利用したキー”secret”を利用して確認を行っています。正しい場合はTokenに含まれていたpayloadを戻しています。確認に失敗した場合(Tokenが正しくない場合)はステータスコードの403のForbiddenが戻されるように設定を行なっています。


app.get('/api/auth/user/', async (req, res) => {
  try {
    const bearToken = await req.headers['authorization'];
    const bearer = await bearToken.split(' ');
    const token = await bearer[1];

    const user = await jwt.verify(token, 'secret');
    res.json({ user });
  } catch (err) {
    res.sendStatus(403)
  }
});
Tokenを確認した後のJSONでは戻すオブジェクトの名前はuserに設定します。nuxt/authではデフォルトでuserという名前で認識します。

AuthrizationのBearの後にTokenを設定していますが設定しているTokenはログインのPOSTリクエストを実行しサーバから戻されるTokenを設定しています。


###
GET http://127.0.0.1:5000/api/auth/user/
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZGM3NjAyMzUyNDg3MzJlNjE2OTdjYiIsIm5hbWUiOiJrZXZpbiIsImVtYWlsIjoia2V2aW5AdGVzdC5jb20iLCJpYXQiOjE2MjUxMDM4Njl9.-jommJm0GdvVRB8VCk-qQDAhmU3Mbnjqm1VUQEQ3RtI

Tokenの確認がサーバ側で行われ問題がない場合はpayloadで設定した内容が戻されます。


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 102
ETag: W/"66-n2qMcmM2/1dvRoFzAJzV5XsBlog"
Date: Thu, 01 Jul 2021 01:48:00 GMT
Connection: close

{
  "decoded": {
    "id": "60dc760235248732e61697cb",
    "name": "kevin",
    "email": "kevin@test.com",
    "iat": 1625103869
  }
}

正しくないTokenを入力した場合は403 Forbiddenが戻ってくることも確認しておきます。


HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 9
ETag: W/"9-PatfYBLj4Um1qTm5zrukoLhNyPU"
Date: Thu, 01 Jul 2021 01:51:46 GMT
Connection: close

Forbidden

mongoDBを利用した場合のJWTの設定方法を確認することができました。