【BTP】Node.jsとPostgreSQLでシンプルなアプリケーションを作成（3）Approuter編

##はじめに
この記事は、[Node.jsとPostgreSQLでシンプルなアプリケーションを作成](https://qiita.com/tami/items/267cf5a32ce0547eaa46)するシリーズの3回目です。

##この記事のゴール

- Approuterを使用して認証を行う
- Approuterを介さないサービスへのアクセスをブロックする
- CRUD処理に必要な権限をチェックする

##ステップ

1. Approuterを使用して認証を行う
1. Approuterを介さないサービスへのアクセスをブロックする
1. CRUD処理に必要な権限をチェックする

###1. Approuterを使用して認証を行う
####1.1. Approuterモジュールを追加
プロジェクト直下にapprouterというフォルダを追加します。

```
node-postgres-sample
　└ approuter
　└ srv
　└ mta.yaml
```

####1.2. approuter/package.jsonの設定
approuterフォルダにpackage.jsonを追加します。

```json:approuter/package.json
{
  "name": "approuter",
  "version": "1.0.0",
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  },
  "dependencies": {
    "@sap/approuter": "^10.4.0"
  }
}
```

####1.3. approuter/xs-app.jsonの設定
approuterフォルダにxs-app.jsonファイルを追加します。このファイルによって入ってきたリクエストをNode.jsアプリのURLにルーティングします。

```json:approuter/xs-app.json
{
    "welcomeFile": "index.html",
    "authenticationMethod": "route",
    "routes": [
        {
            "source": "^/node-pg/",
            "target": "/",
            "authenticationType": "xsuaa",
            "destination": "srv-api"
        }
     ]
  }
```

welcolmeFile(index.html)は、approuter/resourcesの中に作成します。これはApprouterのURLを叩いたときに最初に表示されるページです。

```html:approuter/resources/index.html
<body>
    Welcome to App Router default web page <br>
</body>
```

####1.4 mta.yamlの調整
mta.yamlにApprouterモジュールを追加します。

```yaml:mta.yaml
modules:
 - name: node-postgres-sample-approuter
   type: approuter.nodejs
   path: approuter
   parameters:
     disk-quota: 512M
     memory: 512M
   requires:
     - name: node-postgres-sample-uaa
     - name: srv-api
       group: destinations
       properties:
         name: srv-api
         url: "~{srv-url}"
         forwardAuthToken: true  
```

resourcesセクションにXSUAAサービスを追加します。

```yaml
  - name: node-postgres-sample-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      path: ./xs-security.json
      service-plan: application
      service: xsuaa    
```

プロジェクト直下にxs-security.jsonを作成します。これはXSUAAの設定用のファイルです。

```json:xs-security.json
{
    "xsappname": "node-postgres-sample",
    "tenant-mode": "dedicated",
    "scopes": [
      {
        "name": "uaa.user",
        "description": "UAA"
      }
    ],
    "role-templates": [
      {
        "name": "Token_Exchange",
        "description": "UAA",
        "scope-references": [
          "uaa.user"
        ]
      }
    ]
  }
```

####1.5 ビルド、デプロイ
ビルド、デプロイした後、シークレットモードでApprouterのURLにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/dd610c25-8ee2-0b4c-6e68-d14cff43d312.png)
ログインを求められるので、XSUAAを使用した認証が有効になっていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0c9a3ce9-7977-4a99-ca2d-adda3ad957f2.png)
ログインすると、Welcomeページが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/88f8fc7b-68f9-52ce-bde8-4302aef87c99.png)
URLの末尾を`/node-pg/products`とすると、productsのデータにアクセスできます。これでNode.jsのアプリケーションにリクエストが転送されていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/916ce9e3-2eed-dbba-d31c-52c46701a385.png)

もともとのサービスのURLをシークレットモードで開いてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d8d3358d-522b-de0a-57f0-91515a73eb1a.png)
こちらは認証なしでアクセスできます。現状、直接サービスのURLを叩いた場合は認証されていないユーザでもアクセスできる状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7fcbae8a-f9fd-d0d7-dfcf-32459491cbb6.png)

###2. Approuterを介さないサービスへのアクセスをブロックする
####2.1. Dependencyを追加
`srv`フォルダに移動し、以下のDependencyを追加します。

```
npm i @sap/xssec passport
```

- [@sap/xssec](https://www.npmjs.com/package/@sap/xssec): HTTPヘッダに渡されたアクセストークンを検証したり、認証情報にアクセスするためのモジュール
- [passport](https://www.npmjs.com/package/passport): 認証を行うためのミドルウェア。任意の[Strategy]([https://www.npmjs.com/package/passport#strategies)を利用して認証を行うことができる

package.jsonのdependenciesセクションは以下のようになります。

```json:srv/package.json
  "dependencies": {
    "@sap/xsenv": "^3.1.0",
    "@sap/xssec": "^3.2.1",
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "passport": "^0.4.1",
    "pg-promise": "^10.10.2"
  }
```

####2.2. passportを使用したトークンチェックの追加
server.jsに以下のコードを追加します。

```javascript:srv/server.js
'use strict';

const express = require('express')
const bodyParser = require('body-parser')
//追加--------------------------------------------------
const passport = require('passport')
const JWTStrategy = require('@sap/xssec').JWTStrategy
const xsenv = require('@sap/xsenv')
//------------------------------------------------------

const dbConn = require('./db-conn')
const dbOp = require('./db-op')

var _db = undefined
const app = express()

app.use(bodyParser.json())
//追加--------------------------------------------------
passport.use(new JWTStrategy(xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa));
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
//------------------------------------------------------
```

リクエストヘッダにJWTトークンが存在し、認証が成功すると以下のオブジェクトにアクセス可能になります。これらのオブジェクトを追加の権限チェック（たとえば、ユーザが指定したスコープを持っているか）に使うことができます。（[@sap/xssec](https://www.npmjs.com/package/@sap/xssec)の"Usage with Passport Strategy"セクションを参照）

|オブジェクト  |説明  |
|---|---|
|request.user  |ユーザのID(id)、名前(name)、メールアドレス(emails)などの情報  |
|request.authInfo  |[Security Context](https://www.npmjs.com/package/@sap/xssec#api-description)のオブジェクト。利用可能なメソッドについてはリンク先の"API Description"のセクションを参照。  |
|request.tokenInfo  |TokenInfoオブジェクト（※）。トークンの情報にアクセスできる。 |

※[ドキュメント](https://www.npmjs.com/package/@sap/xssec)のTokenInfoオブジェクトに関するリンクが切れていたので、持っているメソッドについて確認した。結果は以下の通り。

- reset
- isDecoded
- isValid
- getErrorObject
- getTokenValue
- getHeader
- getPayload
- getExpirationDate
- getIssuedAt
- getIssuer
- getSubject
- getAudiencesArray
- getUserId
- getZoneId
- getClientId
- isTokenIssuedByXSUAA
- verify 


####2.3 mta.yamlの調整
`node-postgres-sample-srv`のrequresセクションにnode-postgres-sample-uaaを追加します。

```yaml:mta.yaml
 - name: node-postgres-sample-srv
   type: nodejs
   path: srv
   provides:
    - name: srv-api
      properties:
        srv-url: ${default-url}
   build-parameters:
     ignore: ["node_modules/"]        
   requires:
    - name: cap-posgre-sample-db
    - name: node-postgres-sample-uaa  
```

####2.3 ビルド、デプロイ
ビルド、デプロイした後、シークレットモードでサービスのURLにアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fe0afa7d-50d7-b3a4-5385-20527c4810ca.png)

Unauthorizedとなりました。サービスのURLを直接叩いた場合はアクセスできなくなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e8f7c373-246e-1505-acde-e63334d8ce79.png)

Approuterを経由した場合はアクセス可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e802b53f-df2c-ab53-aad7-e6c2db3c70c5.png)

###3. CRUD処理に必要な権限をチェックする
サービスで実行可能な操作は以下の通りです。各操作に対し、必要な権限（スコープ）を設定します。

|URI  |HTTPメソッド|操作|必要な権限（スコープ）  |
|---|---|---|---|
|/　|GET　|"Hello!"を表示　|-　|
|/products　|GET　|すべてのproductを表示　|DisplayまたはUpdate　|
|/products/:id　|GET　|指定したidのproductを表示　|DisplayまたはUpdate　|
|/products　|POST　|productを登録　|Update　|
|/products　|PUT　|productを変更　|Update　|
|/products　|DELETE　|productを削除　|Update　|

####3.2. xs-security.jsonを更新
xs-security.jsonでDisplay、およびUpdate用のスコープとそれらのスコープを持つロールテンプレートを定義します。

```json:xs-security.json
{
    "xsappname": "node-postgres-sample",
    "tenant-mode": "dedicated",
    "scopes": [
      {
        "name": "$XSAPPNAME.Display",
        "description": "Display Products"
      },
      {
        "name": "$XSAPPNAME.Update",
        "description": "Update Products"
      }
    ],
    "role-templates": [
      {
        "name": "Viewer",
        "description": "View Products",
        "scope-references": [
          "$XSAPPNAME.Display"
        ]
      },
      {
        "name": "Manager",
        "description": "Maintain Products",
        "scope-references": [
          "$XSAPPNAME.Display",
          "$XSAPPNAME.Update"
        ]
      }
    ]
  }
```


####3.3. approuter/xs-app.jsonを更新
特定の操作（URIとHTTPメソッドの組み合わせ）に対して、ユーザが所定のスコープを持っているかどうかをApprouterでチェックします。

```json:approuter/xs-app.json
{
    "welcomeFile": "index.html",
    "authenticationMethod": "route",
    "routes": [
        {
            "source": "^/node-pg/products(.*)$",
            "target": "/products$1",
            "authenticationType": "xsuaa",
            "destination": "srv-api",
            "scope": {
                "GET": ["$XSAPPNAME.Display", "$XSAPPNAME.Update"],
                "default": "$XSAPPNAME.Update"
            }            
        },
        {
            "source": "^/node-pg/",
            "target": "/",
            "authenticationType": "xsuaa",
            "destination": "srv-api"
        }                  
     ]
  }
```
`products`で始まるURIとそれ以外で権限チェックの有無が変わるので、ルートを分けています。

- `products`で始まるURIの場合（1つ目のルート）
    - GETリクエストに対して、ユーザが`$XSAPPNAME.Display`または`$XSAPPNAME.Update`のスコープを持っているかチェックする・・・scope.GET
    - それ以外のHTTPメソッドに対しては`$XSAPPNAME.Update`のスコープを持っているかをチェックする・・・scope.default
- `products`始まりでないURIの場合（2つ目のルート）
    - スコープのチェックは行わない

####3.3 ビルド、デプロイ
ビルド、デプロイした後、シークレットモードでApprouterのURLにアクセスします。

`/node-pg/products`にアクセスすると、Forbiddenとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0f9e1cf0-c76c-3916-83e5-f8739a39299c.png)

`/node-pg/products/1`も同様です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6a548513-7c44-2867-58ff-b8125fc03c5a.png)

`/node-pg/`にはアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/52975778-6c0a-da5d-ca61-0c15975e480f.png)

####3.4 ユーザにロールを割り当て
デプロイの結果、Approuterのアプリケーションに対して2つのロールテンプレートが作成されています。まずは、Viewerロールを自分のユーザに割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d6cb106f-b889-c450-a0e3-3f1c677a753c.png)

Security>Role Collectionsのメニューからロールコレクションを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/298829aa-c9a4-ecd2-1ed4-3e60bf17a636.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/372e6ee3-05d0-50e2-b42e-8348b42fc360.png)
ロールコレクションにViewerロールと、自分のユーザを割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6009379a-9e73-5c06-777c-ef48ac00ebf5.png)

ロールを割り当てた後、シークレットウインドウからログインしなおし、`/node-pg/products`にアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f1e8a757-cf4b-747f-5e3c-a4ab9b81f751.png)
`/node-pg/products/1`にもアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c5a10050-e877-c9b9-ad0d-84752d6c2494.png)

POST、PUT、DELETEについてはブラウザからテストができないため、UIを作成してから確認することにします。

##まとめ
この記事では、以下を実施しました。

- Approuterを使用して認証を行う
- Approuterを介さないサービスへのアクセスをブロックする
- CRUD処理に必要な権限をチェックする

最終回となる[次回](https://qiita.com/tami/items/56651f04d9eb9c3db2c4)はサービスを使用したUIを作成したいと思います。

##参考

- [SAP-samples/cloud-cf-helloworld-nodejs/Chapter 3: Prepare authentication for Node.js RESTful API](https://github.com/SAP-samples/cloud-cf-helloworld-nodejs/tree/3_Security_with_AppRouter)
- [SAP-samples/cloud-cf-helloworld-nodejs/Chapter 4: Security Insight](https://github.com/SAP-samples/cloud-cf-helloworld-nodejs/tree/4_Secure_Application_Mandatory_Authentication)
- [SAP-samples/cloud-cf-helloworld-nodejs/Chapter 5: Authorization - Static Checks and Configuration](https://github.com/SAP-samples/cloud-cf-helloworld-nodejs/tree/5_Secure_Application_Authorization_Static_Check)