RESTful APIでAuth0 Management APIのWrapperを作成する

# はじめに

Auth0は[Management API](https://auth0.com/docs/api/management/v2)を公開していてダッシュボードで出来る管理作業(例/API Clientの作成や削除)はManagement APIでも可能です。この記事はManagement API用のWrapperをRESTful APIで作成する手順(下のダイアグラムの点線内を作成します)で、[こちらの原文](https://auth0.com/blog/rest-architecture-part-1-building-api/)を元に作成しています。nodeとnpmのインストール、Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。まだの方は[こちら](https://qiita.com/furuth/items/8826b66a3ec2b2b14222)の記事を参照の上ご準備をお願いします。
<img src="https://cdn.auth0.com/blog/building-restful-systems/auth0-dashboard-project.png" width=70%>
完成版のソースコードは[こちら](https://github.com/auth0-blog/auth0-restful-dashboard)にあります。

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

# 手順
## プロジェクトの準備
[GitHubリポジトリ](https://github.com/auth0-blog/auth0-restful-dashboard)をローカルにクローンします。

```consle
$ git clone https://github.com/auth0-blog/auth0-restful-dashboard.git
```

nodeのパッケージをインストールします。

```console
$ cd auth0-restful-dashboard
$ cd auth0-restful-management-api
$ npm install
```

## WrapperをAuth0のApplicationとして登録
Auth0ダッシュボードの左ペインから"Applications"をクリック、右上の"CREATE APPLICATION"を押します。
<img src="https://i.imgur.com/ZtGP5PL.png" width=70%>

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

"Auth0 Management API"を選択、Scopeは"read:clients", "update:clients"を選択して"AUTHORIZE"を押します。
<img src="https://i.imgur.com/nkmyit3.png" width=70%>

~/auth0-restful-management-api/.envを作成します。

```console
$ cd ~/auth0-restful-management-api
$ vi .env
```

環境変数を設定します。**"CLIENT_DOAMIN"はAuth0のテナント名、"CLIENT_ID", "CLIENT_SECRET"は作成したApplicationをクリックして"Setting"タグから確認できます。**

```vim:.env
CLIENT_DOMAIN="kiriko.auth0.com"
CLIENT_ID="xxxxxxxx"
CLIENT_SECRET="xxxxxxxx"
```

## RESTful API Gatewayの作成
~/auth0-restful-management-api/index.jsをバックアップして空のindex.jsを作成します。**何をどのように何故やるのかを理解するためスクラッチで実装します。**

```console
$ cd ~/auth0-restful-management-api
$ cp index.js index.js.org
$ touch index.js
```

index.jsを作成します。**Management APIのRoot EndPointとClient Listを取得するFunctionを実装しています。**

```jsx:index.js
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

//Define the root endpoint
app.get(apiPrefix, async (req, res) => {
    const clientsState = await getClients();

    res.send(clientsState);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}
```

Root EndPointを起動してリクエストを投げてみます。

```console
$ pwd
~/auth0-restful-management-api
$ node index.js
listening on port 3001
```

```console
$ curl localhost:3001/api
```

```json
{
  "resourceType": "client-list",
  "clients": [
    {
      "id": "xxxxxxxx",
      "name": "client hogehoge",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
    {
      "id": "xxxxxxxx",
      "name": "client foobar",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
    {
      "id": "xxxxxxxx",
      "name": "client mokomoko",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
----省略----
}
```

特定のClientを取得するFunctionを追加します。以下、追加後のindex.jsです。

```jsx:index.js
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

//Define the root endpoint
app.get(apiPrefix, async (req, res) => {
    const clientsState = await getClients();

    res.send(clientsState);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}
```

getClient()をAPI EndPointから呼び出せるようにコードを修正します。以下、修正後のindex.jsです。

```jsx:index.js
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

//Define the root endpoint
app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}
```
ClientをUpdateする処理を追加します。以下、追加後のindex.jsです。

```jsx:index.js
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');
const bodyParser = require('body-parser');  //import the body-parser library

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

app.use(bodyParser.json());         // enable the body-parser middleware

//Define the root endpoint
app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}
```

ClientをアップデートするFunctionを追加します。以下、追加後のindex.jsです。

```jsx:index.js
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');
const bodyParser = require('body-parser');  //import the body-parser library

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

app.use(bodyParser.json());         // enable the body-parser middleware

//Define the root endpoint
app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}

async function updateClient(clientId, updatedData) {
    await Auth0Manager.init();
    await Auth0Manager.updateClient(updatedData, clientId);
}
```

updateClient()をAPI EndPointから呼び出せるようにコードを修正します。以下、修正後のindex.jsです。

```jsx
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');
const bodyParser = require('body-parser');  //import the body-parser library

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

app.use(bodyParser.json());         // enable the body-parser middleware

//Define the root endpoint
app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.put(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const updatedData = req.body;

    await updateClient(req.params.clientId, updatedData);

    res.sendStatus(200);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}

async function updateClient(clientId, updatedData) {
    await Auth0Manager.init();
    await Auth0Manager.updateClient(updatedData, clientId);
}
```

Root EndPointを起動してリクエストを投げてみます。

```console
$ pwd
~/auth0-restful-management-api
$ node index.js
listening on port 3001
```

Applicationの"description"をアップデートするリクエストです。**xxxxxxxxはターゲットのClient IDを指定します。**

```console
curl -H 'Content-Type: application/json' -X PUT -d '
{"description": "This is my app"}'  http://localhost:3001/api/clients/xxxxxxxx
```

リクエストが成功した確認します。

```console
$ curl localhost:3001/api/clients/xxxxxxxx | jq .
```

```json
{
  "resourceType": "client",
  "id": "xxxxxxxx",
  "name": "Qiita  Management API Wrapper Mock",
  "description": "This is my app",
  "links": [
    {
      "uri": "/api/clients/xxxxxxxx",
      "rel": "self"
    },
    {
      "uri": "/api/clients/xxxxxxxx",
      "rel": "self",
      "type": "PUT"
    }
  ]
}
```

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

"Name", "Identifier"に任意の名前を入力、"Signing Algorithmはデフォルトのまま"CREATE"を押します。**Identifierは実在するURLである必要はありません。管理し易い任意の名前を入力します。**
<img src="https://i.imgur.com/W18TcBm.pngd" width=70%>

.envに環境変数を追加します。

```console
$ cd ~/auth0-restful-management-api
$ vi .env
```

```vim
CLIENT_DOMAIN="kiriko.auth0.com"
CLIENT_ID="xxxxxxxx"
CLIENT_SECRET="xxxxxxxx"
API_IDENTIFIER="https://restful-dashboard-api"
```

access_tokenの管理するために必要なパッケージをインストールします。

```console
$ pwd
~/auth0-restful-management-api
$ npm install express-jwt jwks-rsa
```

index.jsにインストールしたパッケージをインポートする処理を追加します。以下、追加後のindex.jsです。

```jsx
//index.js
const express = require('express');
const Auth0Manager = require('./Auth0Manager');
const bodyParser = require('body-parser');  //import the body-parser library
const jwt = require('express-jwt');     //<--- newly added
const jwksRsa = require('jwks-rsa');    //<--- newly added

require('dotenv').config();

const app = express();
const apiPrefix = '/api';

app.use(bodyParser.json());         // enable the body-parser middleware

//Define the root endpoint
app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.put(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const updatedData = req.body;

    await updateClient(req.params.clientId, updatedData);

    res.sendStatus(200);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

//Use the Auth0 Management API wrapper and build the response with hypermedia 
async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}

async function updateClient(clientId, updatedData) {
    await Auth0Manager.init();
    await Auth0Manager.updateClient(updatedData, clientId);
}
```

Auth0から発行されるJWT形式のaccess_tokenをチェックする処理を追加します。以下、追加後のindex.jsです。

```jsx
const express = require('express');
const bodyParser = require('body-parser');
const Auth0Manager = require('./Auth0Manager');
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

require('dotenv').config();
const apiPrefix = '/api';

const app = express();

app.use(bodyParser.json());

const checkJwt = jwt({
    secret: jwksRsa.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${process.env.CLIENT_DOMAIN}/.well-known/jwks.json`
    }),  
    audience: process.env.API_IDENTIFIER,
    issuer: `https://${process.env.CLIENT_DOMAIN}/`,
    algorithms: ['RS256']
  });

app.use(checkJwt);

app.get(apiPrefix, async (req, res) => {
    const clientsState = await getClients();

    res.send(clientsState);
});

app.get(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const clientState = await getClient(req.params.clientId);

    res.send(clientState);
});

app.put(`${apiPrefix}/clients/:clientId`, async (req, res) => {
    const updatedData = req.body;

    await updateClient(req.params.clientId, updatedData);

    res.sendStatus(200);
});

app.listen(3001, () => {
    console.log('listening on port 3001');
});

async function getClients() {
    await Auth0Manager.init();

    const clients = await Auth0Manager.getClients();
    const clientList = clients.map(client => ({
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "client"
        }]
    }));

    return { resourceType: "client-list", clients: clientList };
}

async function getClient(clientId) {
    await Auth0Manager.init();

    const client = await Auth0Manager.getClient(clientId);

    return {
        resourceType: "client",
        id: client.client_id,
        name: client.name,
        description: client.description || "",
        links: [{
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self"
        },
        {
            uri: `${apiPrefix}/clients/${client.client_id}`,
            rel: "self",
            type: "PUT"
        }]
    };
}

async function updateClient(clientId, updatedData) {
    await Auth0Manager.init();
    await Auth0Manager.updateClient(updatedData, clientId);
}
```

Root EndPointを起動してリクエストを投げてみます。**RESTful APIはAuth0で保護されておりaccess_tokenが必要なためエラーになることを確認します。**

```console
$ pwd
~/auth0-restful-management-api
$ node index.js
listening on port 3001
```

```console
$ curl localhost:3001/api
listening on port 3001
UnauthorizedError: No authorization token was found
    at middleware (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express-jwt/lib/index.js:76:21)
    at Layer.handle [as handle_request] (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/layer.js:95:5)
    at trim_prefix (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:317:13)
    at /Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:275:10)
    at jsonParser (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/body-parser/lib/types/json.js:110:7)
    at Layer.handle [as handle_request] (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/layer.js:95:5)
    at trim_prefix (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:317:13)
    at /Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:275:10)
    at expressInit (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/layer.js:95:5)
    at trim_prefix (/Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:317:13)
    at /Users/hisashiyamaguchi/auth0-restful-dashboard/auth0-restful-management-api/node_modules/express/lib/router/index.js:284:7
```

Auth0ダッシュボードの左ペイン"APIs->作成したAPI"をクリック、"Test"タブをクリックします。
<img src="https://i.imgur.com/DjfbB2M.png" width=70%>

画面中頃の"Response"でaccess_tokenをコピーします。
<img src="https://i.imgur.com/P46dKN4.png" width=70%>

access_token付きでリクエストを投げます。Clientリストが取得できれば成功です。

```console
$ curl -H "Authorization: Bearer eyJ0xxxxxxxx" localhost:3001/api|jq .
```

```json
{
  "resourceType": "client-list",
  "clients": [
    {
      "id": "xxxxxxxx",
      "name": "client hogehoge",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
    {
      "id": "xxxxxxxx",
      "name": "client foobar",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
    {
      "id": "xxxxxxxx",
      "name": "client mokomoko",
      "description": "",
      "links": [
        {
          "uri": "/api/clients/xxxxxxxx",
          "rel": "client"
        }
      ]
    },
----省略----
}
```

# おわりに
この記事では例としてAuth0 Management APIのread:client, update:clientのみを対象のScopeとしていますが、もちろん他のScopeを対象とすることも可能です。ユーザ管理系のScope(ex/read:users, update:users)を対象とすればセルフサービスユーザ管理ポータルを簡単に実装することもできますね。