HTTP APIへのアクセスをAuth0のRBAC機能を利用して制御する

# はじめに

この記事はHTTP APIへのアクセスをRBAC(Role Based Access Control)で制御する手順で、[こちらの原文](https://auth0.com/blog/authorization-series-pt-2-securing-http-apis-with-rbac-rules/)を元に作成しています。nodeとnpmのインストール、Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。まだの方は[こちら](https://qiita.com/furuth/items/8826b66a3ec2b2b14222)の記事を参照の上ご準備をお願いします。

# 環境
- **OS** : `macOS Mojave 10.14.6`
- **node** : `10.15.3`
- **npm** : `6.11.3`

# 手順
## APIの登録
Auth0ダッシュボードの左ペインで"APIs"をクリック、右上の"CREATE API"を押します。
<img src="https://i.imgur.com/6dwCa0a.png" width=70%>

**休暇申請を行うAPIを想定しています。**"Name"に"Vacation API" "Identifier"に"`https://vacation-api.troubleshoo.com`"を入力、"Signing Algorithmはデフォルトのまま"CREATE"を押します。**Identifierは実在するURLである必要はありません。管理し易い任意の名前を入力します。**
<img src="https://i.imgur.com/rpfN7Rz.png" width=70%>

"Permissions"タブをクリック、左のテキストボックスに"create:vacations"、右のテキストボックスに"Create vacations requests."を入力して"ADD"を押します。**APIのScope(Permission)を定義しています。左のテキストボックスに入力するScopeの書式はxx:xxである必要があります。**
<img src="https://i.imgur.com/GEZzWVL.png" width=70%>

同様に"read:vacations"、"Read vacation requests."を入力して"ADD"を押します。
<img src="https://i.imgur.com/RHJbKxE.png" width=70%>

"Settings"タブをクリックして"Enable RBAC"、"Add Permissions in the Access Token"フリップスイッチをオンにして"SAVE"を押します。**Auth0のRBAC機能を有効化して、ユーザが認証されたタイミングでaccess_tokenに認可されたAPIのScopeを含めます。**
<img src="https://i.imgur.com/EAR1uS6.png" width=70%>

同様にExpense API, Invoice APIを作成します。
<img src="https://i.imgur.com/3DWAEkt.png" width=70%>
<img src="https://i.imgur.com/5Dac3h1.png" width=70%>

## Roleの作成
左ペインの"Users&Roles"->Roles"をクリック、"CREATE ROLE"を押します。
<img src="https://i.imgur.com/4sPp5KX.png" width=70%>

"Name"に"Vacation Requester"、"Description"に"Role that gives users permissions to request vacations."を入力して"CREATE"を押します。
<img src="https://i.imgur.com/hy2YNno.png" width=70%>

"Permissions"タブをクリックして"ADD PERMISSIONS"をクリックします。
<img src="https://i.imgur.com/XkcG4SP.png" width=70%>

"Vacation API"を選択して"Select All"をクリック、"ADD PERMISSIONS"を押します。**RoleにAPIのScopeを紐付けしています。**
<img src="https://i.imgur.com/caGYI0U.png" width=70%>

同様にExpense Submitter, Invoice Submitterを作成します。
<img src="https://i.imgur.com/UMCbMs8.png" width=70%>
<img src="https://i.imgur.com/UEyQboT.png" width=70%>

## Userの作成とRoleの割り当て
左ペインの"Users&Roles"->Users"をクリック、"CREATE USER"を押します。Email/Passwordを入力して"CREATE"を押します。
<img src="https://i.imgur.com/ACemIZs.png" width=70%>

"Roles"タブをクリックして"ASSIGN ROLES"を押します。
<img src="https://i.imgur.com/bXVhBux.png" width=70%>

"Vacation Requester", "Expense Submitter"を選択して"ASSIGN"を押します。**UserにRoleを割り当てています。**
<img src="https://i.imgur.com/hGwAzNB.png" width=70%>

同様に別のユーザを作成して"Expense Submitter", "Invoice Submitter"をアサインします。
<img src="https://i.imgur.com/2yVG7t4.png" width=70%>

## Applicationの登録
左ペインの"Applications"をクリックして右上の"CREATE APPLICATION"を押します。
<img src="https://i.imgur.com/ZtGP5PL.png" width=70%>

任意の名前を入力して"Choose an application type"で"Single Page Web Applications"を選択して"CREATE"を押します。
<img src="https://i.imgur.com/70rHJkw.png" width=70%>

"Settings"タブをクリック、"Allowed Callback URLs"に"`https://troubleshoo.now.sh/callback`", "Allowed Web Origins"と"Allowed Logout URLS"に"`https://troubleshoo.now.sh`"を入力して"SAVE CHANGES"を押します。**Auth0で認証後にリダイレクトできるURLを申告しています。**
<img src="https://i.imgur.com/z2d52hp.png" width=70%>

## API/Applicationの実装
[API/ApplicationのGitHubリポジトリ](https://github.com/auth0-blog/rbac-nodejs)をCloneします。

```console
$ git clone git clone https://github.com/auth0-blog/rbac-nodejs.git
```

PackageをインストールしてAPIとApplicationを起動します。

```console
$ cd rbac-nodejs
$ npm install
$ npm start
```

APIにリクエストを投げます。**Auth0のRBAC機能で保護していないためリクエストが成功することを確認しています。**

```console
$ curl http://localhost:3001/vacations
```

```console
"GET /vacations HTTP/1.1" 200 2 "-" "curl/7.54.0"
```

rbac-nodejs/srcにauthorizationUtils.jsを作成します。**access_tokenとpermissionをチェックするFunctionを実装しています。**

```jsx:authorizationUtils.js
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

function checkAccessToken(issuer, audience) {
  return jwt({
    secret: jwksRsa.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${issuer}/.well-known/jwks.json`
    }),

    // Validate the audience and the issuer.
    audience,
    issuer: `https://${issuer}/`,
    algorithms: ['RS256']
  })
}

function checkPermission(permission) {
  return (req, res, next) => {
    const {permissions} = req.user;
    if (permissions.includes(permission)) return next();
    res.status(403).send();
  }
}

module.exports = {
  checkAccessToken,
  checkPermission,
};
```

rbac-nodejs/src/apisのinvoiceAPI.js, expensesAPI.js, vacationAPI.jsを修正します。**Auth0のRBAC機能で保護しています。**


```jsx:invoiceAPI.js
const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertInvoice, getInvoices} = require('../database/invoices');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.INVOICE_API));

// endpoint to return all invoice requests
router.get('/', checkPermission('read:invoices'), async (req, res) => {
  res.send(await getInvoices());
});

// endpoint to insert new invoice request
router.post('/', checkPermission('create:invoice'), async (req, res) => {
  const newInvoice = req.body;
  await insertInvoice(newInvoice);
  res.send({ message: 'Invoice request inserted.' });
});

module.exports = router;
```

```jsx:expensesAPI.js
const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertExpense, getExpenses} = require('../database/expenses');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.EXPENSE_API));

// endpoint to return all expense reports
router.get('/', checkPermission('read:expenses'), async (req, res) => {
  res.send(await getExpenses());
});

// endpoint to insert new expense report
router.post('/', checkPermission('create:expenses'), async (req, res) => {
  const newExpense = req.body;
  await insertExpense(newExpense);
  res.send({ message: 'Expense report inserted.' });
});

module.exports = router;
```

```jsx:vacationsAPI.js
const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertVacation, getVacations} = require('../database/vacations');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.VACATION_API));

// endpoint to return all vacation requests
router.get('/', checkPermission('read:vacations'), async (req, res) => {
  res.send(await getVacations());
});

// endpoint to insert new vacation request
router.post('/', checkPermission('create:vacations'), async (req, res) => {
  const newVacation = req.body;
  await insertVacation(newVacation);
  res.send({ message: 'Vacation request inserted.' });
});

module.exports = router;
```

rbac-nodejsに.envを作成します。**ISSUERはAuth0のテナント名、xxx_APIは各々のAPI Identifierを指定します。**

```
ISSUER=kiriko.auth0.co
EXPENSE_API=https://expense-api.troubleshoo.com
INVOICE_API=https://invoice-api.troubleshoo.com
VACATION_API=https://vacation-api.troubleshoo.com
```

APIとApplicationを起動してChromeで`https://https://troubleshoo.now.sh`にアクセスします。

```console
$ cd rbac-nodejs
$ npm start
```

トップページで"Config"をクリックします。
<img src="https://i.imgur.com/3GtALBn.png" width=70%>

パラメータを設定して"SAVE"を押します。"Auth0 Client ID"はAuth0ダッシュボードで登録したApplicationのClient IDを指定します。
<img src="https://i.imgur.com/1SJLfBR.png" width=70%>

右上の"Sign In"を押してログインします。
<img src="https://i.imgur.com/XHSvVsK.png" width=70%>

Roleで設定したAPIを操作できることを確認します。
<img src="https://i.imgur.com/shtsVx0.png" width=70%>

# おわりに
従来型のRBACの実装は、プロジェクト毎にRDBMSにUser-Group-Role-Permissionの紐付けを作成するケースが多いのではないでしょうか。開発者は要件が変わる度にテーブルスキーマを変更して、テストして、バグ改修してようやくリリース、リリース後のインシデント対応、HotFix作成、また要件が変わってテーブルスキーマ再設計。。。とRBACの運用と改修対応に追われてしまい本来やるべき活動(ソフトウェアで社会を変革する)ができず苦慮されていると思います。RBACはAuth0に任せて本来やるべきことに集中頂きたいですね。