Node.js APIで認証付きのMockREST APIサーバの導入

##はじめに
@ochiochi さんの記事に触発され、Node.jsでWebAPIの認証付きのMockWebAPI（JSON Server）を作成してみました。

http://qiita.com/ochiochi/items/966b884eb17045dfb929

##JSON Server
JSON Serverは、Node.jsのモジュールで、簡単にREST APIサーバが導入できます。
（@ochiochi さんの紹介）

https://github.com/typicode/json-server

JSON Serverは、Express.jsをベースにしているので、認証機能等を追加する場合には、Express.js用のプログラムを作成します。

## JSON Server導入

### 1. node module

```
$ npm install json-server -g
```
### 2. Mock データベースファイルの作成
REST API によって、操作するデータベースとなるファイルをJSON形式で作成します。

```json:books.json
{
  "books": [
    {
      "id": 1,
      "title": "Math",
      "price": 1000
    },
    {
      "id": 2,
      "title": "Science",
      "price": 1200
    },
    {
      "id": 3,
      "title": "Chemical",
      "price": 1400
    }
  ]
}
```

### 3. REST APIサーバ起動

```
$ json-server --watch ./books.json
```

デフォルトポート３０００で、Webサーバが起動します。

| method | url | request content type |
|:-:|:-:|:-:|
| GET  |  http://localhost:3000/books |   |
| GET  |  http://localhost:3000/books/1 |   |
| POST  |  http://localhost:3000/books | application/json  |
| PATCH  |  http://localhost:3000/books/1 | application/json  |
| PUT  |  http://localhost:3000/books/1 | application/json  |
| DELETE  |  http://localhost:3000/books/1 |   |

Webサーバサーバを停止する場合には、```Ctrl + c```

## 認証機能導入

### 1. node module
認証トークンの作成と、検証するためのモジュールを導入します。

```
$ npm install jsonwebtoken
```

### 2. Mock ユーザ情報データファイルの作成
ログインに必要なユーザ情報を用意します。このファイルは、認証時に使用する目的以外で公開しません。

```json:users.json
{
  "users": [
    {
      "id": 1,
      "name": "hoge",
      "email": "hoge@email.com",
      "password": "hoge"
    },
    {
      "id": 2,
      "name": "foo",
      "email": "foo@email.com",
      "password": "foo"
    },
    {
      "id": 3,
      "name": "bar",
      "email": "bar@email.com",
      "password": "bar"
    }
  ]
}
```

### 2. 認証REST APIプログラムの作成
プログラム内部で、JSON Serverを利用してREST APIサーバを利用します。

```js:auth-api-server.js
//モジュール参照
const fs = require('fs')
const bodyParser = require('body-parser')
const jsonServer = require('json-server')
const jwt = require('jsonwebtoken')

//JSON Serverで、利用するJSONファイルを設定
const server = jsonServer.create()
const router = jsonServer.router('./books.json')

//JSONリクエスト対応
server.use(bodyParser.urlencoded({extended: true}))
server.use(bodyParser.json())

//署名作成ワードと有効期限(1時間)
const SECRET_WORD = 'SECRET1234'
const expiresIn = '1h'

//署名作成関数
const createToken = payload => jwt.sign(payload, SECRET_WORD, {expiresIn})

//署名検証関数（非同期）
const verifyToken = token =>
  new Promise((resolve, reject) =>
    jwt.verify(token, SECRET_WORD, (err, decode) =>
      decode !== undefined ? resolve(decode) : reject(err)
    )
  )

//ユーザDBファイル読み込み
const userdb = JSON.parse(fs.readFileSync('./users.json', 'UTF-8'))

//ログイン関数 true:ok false:ng
const isAuth = ({email, password}) =>
  userdb.users.findIndex(user => user.email === email && user.password === password) !== -1

//ログインRouter
server.post('/auth/login', (req, res) => {
  const {email, password} = req.body

  //ログイン検証
  if (isAuth({email, password}) === false) {
    const status = 401
    const message = 'Incorrect email or password'
    res.status(status).json({status, message})
    return
  }

  //ログイン成功時に認証トークンを発行
  const access_token = createToken({email, password})
  res.status(200).json({access_token})
})

//認証が必要なRouter(ログイン以外全て)
server.use(/^(?!\/auth).*$/, async (req, res, next) => {

  //認証ヘッダー形式検証
  if (req.headers.authorization === undefined || req.headers.authorization.split(' ')[0] !== 'Bearer') {
    const status = 401
    const message = 'Error in authorization format'
    res.status(status).json({status, message})
    return
  }

  //認証トークンの検証
  try {
    await verifyToken(req.headers.authorization.split(' ')[1])
    next()
  } catch (err) {
    //失効している認証トークン
    const status = 401
    const message = 'Error access_token is revoked'
    res.status(status).json({status, message})
  }
})

//認証機能付きのREST APIサーバ起動
server.use(router)
server.listen(3000, () => {
  console.log('Run Auth API Server')
})
```

### 3. 認証REST APIプログラムの起動

```
$ node auth-api-server.js
```

### 4. ログインAPI

ログインAPIを使用して、認証トークンを発行します。以降のREST APIアクセスは、認証トークンをヘッダーに付加します。

| method | url | request content type |
|:-:|:-:|:-:|:-:|
| POST  |  http://localhost:3000/auth/login |  application/json |

* Request Body 

```json
{
  "email": "hoge@email.com",
  "password":"hoge"
}
```

* Response Body (200 OK)

```
{
   "access_token": "XXXXXXXXXXXXXXXX"
}
```

* Response Body (401 Unauthorized)

```
{
  "status": 401,
  "message": "Incorrect email or password"
}
```

### 5. 認証付きのAPI

REST APIアクセスは、認証トークンをヘッダーに付加します。

* Request Header (required access_token)

```
Authorization: Bearer XXXXXXXXXXXXXXXX
```

認証トークンが失効していた場合

* Response Body (401 Unauthorized)

```
{
  "status": 401,
  "message": "Error access_token is revoked"
}
```

認証ヘッダー形式エラーの場合

* Response Body (401 Unauthorized)

```
{
  "status": 401,
  "message": "Error in authorization format"
}
```