【CAP】XSUAAによる認証を追加・ローカルでテスト

##はじめに
以前の記事で、CAPのHello World的なサービスを作成しました。
このサービスは"誰でも"使える状態でしたが、アクセスできるユーザを絞るようにしたいです。
[【CAP】CAPで作ったODataをUI5アプリで使う(1) CAPサービスの作成](https://qiita.com/tami/items/d1b69a93da14e288faf2)

そこで、この記事では以下のテーマについて扱います。
・CAPで作ったODataサービスをCloud Platormで認証されたユーザのみ使用可能にする
・認証の設定をしたうえでローカルでも実行できるようにする

##Cloud Platformでのユーザ認証の仕組み
Cloud Platformで動くアプリケーションでは、ApprouterとXSUAAサービスのコンビネーションにより認証が行われます。以下のページの説明がわかりやすかったので翻訳・引用します。
https://github.com/SAP/cloud-security-xsuaa-integration

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cbb212ec-7f65-7398-7a53-d44785f15893.png)
1. ユーザがWebアプリケーションにブラウザやモバイルデバイスを介してアクセスする
2. Webアプリケーション（典型的なSAP Cloud Platformアプリケーションの場合、Application Router）はOAuthクライアントとして、認証のためにOAuthサーバにリダイレクトする　⇒ユーザはID、パスワードの入力を求められる
3. 認証されるとWebアプリケーションはOAuthサーバから発行されたコードを使ってアクセストークンを要求する
4. Webアプリケーションはアクセストークンを使ってOAuthリソースサーバからデータを取得する。OAuthリソースサーバはオンラインまたはオフラインでトークンを検証する

アクセストークンは**JWT(Json Web Token)**とも呼ばれます。ユーザID、氏名などのほか、ユーザが持つ権限（スコープ）が記載されています。

##CAPプロジェクトに認証を追加
以下の記事で作成したCAPプロジェクトに認証を追加します。
[【CAP】CAPで作ったODataをUI5アプリで使う(1) CAPサービスの作成](https://qiita.com/tami/items/d1b69a93da14e288faf2)

＜ステップ＞
1. アクセスコントロール用のアノテーションを追加
2. モックの認証を設定（ローカル実行用）
3. トークンを使用した認証を設定（ローカル、本番実行用）

###1. アクセスコントロール用のアノテーションを追加
もともとあったproduct-service.cdsに加え、admin-service.cdsを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4052b247-e0b4-1774-1c85-56b7d0ef4d88.png)
Productサービスは認証されたユーザのみ実行可能、Adminサービスは"admin"ロールを持つユーザのみ実行可能とします。

アクセスコントロールの設定方法については以下のドキュメントをご参照ください。
https://cap.cloud.sap/docs/guides/authorization#roles

```product-service.cds
using demo.products as prod from '../db/schema';

@requires: 'authenticated-user'
service ProductService {
    entity Products as projection on prod.Products;
}
```

```admin-service.cds
using demo.products as prod from '../db/schema';

@requires: 'admin'
service AdminService {
    entity Products as projection on prod.Products;
}
```

`cds watch`で実行してみます。まずProductサービスのProductsエンティティを見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/82a08436-e77e-45f2-3746-07c2fbb91697.png)
認証を求められますが、何も入力せずにサインインしてもデータが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/040ee4e2-d963-bad3-4fe9-4939a5062b29.png)
次に、AdminサービスのProductsエンティティを見てみます。こちらは権限がないのでエラーになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/434ba65e-e4c2-4668-3cff-baeadbacc667.png)

＜ここまででわかったこと＞
ローカル実行の場合、
・authenticated-userが実行できるサービスは、サインインした後は誰でも使える
・特定のロールのみ実行可能なサービスは、そのロールを持つユーザでないと使えない


###2. モックの認証を設定
ローカルでもロールを持ったユーザとして実行できるように、モックの認証の設定をします。
まず、[passport](http://www.passportjs.org/)をプロジェクトに追加します。

```
npm add passport
```

続いて、.cdsrc.jsonファイルを以下のように設定します。これで、adminロールを持つ"admin"というユーザを作成したことになります。（.cdsrc.jsonはプロジェクトのルートに置きます）

```json:.cdsrc.json
{
    "auth": {
        "passport": {
            "strategy": "mock", 
            "users": {
                "admin": { 
                    "password": "dummy", 
                    "ID": "admin",
                    "roles": ["admin", "authenticated-user"]
                }                        
            }
        }
    }    
}
```
再度実行してみましょう。前回のログイン情報が影響しないようにシークレットウインドウで開き、adminユーザでログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f5f29396-c224-cc60-a952-f969d6676633.png)
AdminサービスのProductsエンティティが表示できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f3d334aa-d64d-de04-d045-1d9848d8cf04.png)

###3. トークンを使用した認証を設定
CAPプロジェクトにXSUAAの設定を追加します。最初に本番用の設定をしてCloud Foundryにデプロイしてから、ローカル実行用の設定をします。

####本番用の設定
まず、 @sap/xssecと@sap/xsenvという2つのモジュールをインストールします。
~~※以下のブログによると@sap/xssecのバージョン3はまだサポートされていないのでバージョン2を指定したほうがよさそうです
[CAP: Demystify User Authentication](https://blogs.sap.com/2020/07/20/cap-demystify-user-authentication/)~~

```
npm install @sap/xssec@ @sap/xsenv
```
XSUAA設定用のファイルを生成します。

```
cds compile srv/ --to xsuaa > xs-security.json
```
プロジェクトのルートにxs-security.jsonファイルができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/41d58312-5116-60b6-1663-5760c2e8e243.png)
中身はサービスの内容を参照して自動的に生成されています。

```json:xs-security.json
{
  "xsappname": "products",
  "tenant-mode": "dedicated",
  "scopes": [
    {
      "name": "$XSAPPNAME.admin",
      "description": "admin"
    }
  ],
  "attributes": [],
  "role-templates": [
    {
      "name": "admin",
      "description": "generated",
      "scope-references": [
        "$XSAPPNAME.admin"
      ],
      "attribute-references": []
    }
  ]
}
```

package.jsonのcdsセクションを以下のように設定します。

```json:package.json
  "cds": {
    "requires": {
      "db": {
        "kind": "hana"
      },
      "uaa": {
        "kind": "xsuaa"
      }
    }
  }
```


mta.yamlにXSUAAの設定を追加します。全体は以下のようになります。

```yaml:mta.yaml
## Generated mta.yaml based on template version 0.2.0
## appName = products
## language=nodejs; multiTenant=false
## approuter=
_schema-version: '3.1'
ID: products
version: 1.0.0
description: "A simple CAP project."
parameters:
  enable-parallel-deployments: true
  
  
build-parameters:
  before-all:
   - builder: custom
     commands:
      - npm install
      - npx cds build

modules:
 - name: products-srv
   type: nodejs
   path: gen/srv
   properties:
     EXIT: 1  # required by deploy.js task to terminate 
   requires:
    - name: products-db
    - name: products-uaa
   provides:
    - name: srv-binding      # required by consumers of CAP services (e.g. approuter)
      properties:
        srv-url: ${default-url}

 - name: db
   type: hdb
   path: gen/db  
   parameters:
     app-name: products-db  
   requires:
    - name: products-db
    - name: products-uaa


resources:
 - name: products-db
   type: com.sap.xs.hdi-container
   parameters:
     service: hanatrial  # or 'hanatrial' on trial landscapes
     service-plan: hdi-shared
   properties:
     hdi-service-name: ${service-name}
 - name: products-uaa
   type: org.cloudfoundry.managed-service
   parameters:
     service: xsuaa
     service-plan: application
     path: ./xs-security.json      
```

ビルド&デプロイします。
ビルド： `mbt build`
デプロイ：`cf deploy mta_archives/products_1.0.0.mtar`

この結果、Cloud FoundryにXSUAAサービスのインスタンスが登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5322ff96-5abd-4dfb-0cc0-3fca5e44bdd3.png)

Cloud Fondry環境でサービスを実行するとどうなるか試してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6021e5f5-0bd9-b87b-2013-0a6486233aab.png)
特別な権限のいらないProductサービスの方を実行してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c75017cc-a7e4-cf59-52f1-c078a7c3a2a7.png)
Unauthorized、つまり「認証されていませんよ」というエラーになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6f493bb8-6930-7774-0bb6-82d5aa2f1a34.png)
認証のための入り口が必要ということで、[Approuter](https://help.sap.com/viewer/65de2977205c403bbc107264b8eccf4b/Cloud/en-US/01c5f9ba7d6847aaaf069d153b981b51.html)を追加します。

####Approuterを追加
プロジェクトのルートにapprouterフォルダを追加します。approuterの中に2つのファイル：package.jsonとxs-app.jsonを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/89b7baed-dcf3-34c4-be4d-63400e003e37.png)

```json:approuter/package.json
{
	"name": "app-router",
	"description": "Node.js based application router service",
	"engines": {
		"node": "^8.0.0 || ^10.0.0"
	},
	"dependencies": {
		"@sap/approuter": "6.8.0"
	},
	"scripts": {
		"start": "node node_modules/@sap/approuter/approuter.js"
	}
}
```
mta.yamlにapprouterのモジュールを追加します。

```yaml:mta.yaml
modules:
 - name: products-app-router
   type: approuter.nodejs
   path: approuter
   parameters:
     disk-quota: 256M
     memory: 256M
   requires:
     - name: products-uaa
     - name: srv-binding
       group: destinations
       properties:
         name: srv-binding
         url: "~{srv-url}"
         forwardAuthToken: true  
```
Approuterモジュールは、products-srvのURL(srv-url)をdestinationとして使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/feb50ea6-ca08-3897-db90-5c126a27df16.png)
xs-app.jsonを以下のように設定します。
Approuterにリクエストが入ってきたらsrv-bindingという宛先に流すという意味です。authenticationMethodが"route"となっているので、デフォルトでXSUAAを使用した認証が行われます。

```json:approuter/xs-app.json
{
	"authenticationMethod": "route",
	"routes": [
		{
			"source": "^/(.*)",
			"destination": "srv-binding"
		}
	]
}
```
ここまで設定できたら再度ビルド&デプロイします。

Approuterのアプリケーションを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9a8b546a-6b9f-8b83-0614-3709721bff95.png)
ログインを求められるので、Cloud PlatformのユーザIDとパスワードを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9c7bd392-bb4e-c47f-a9cb-46b87f319b07.png)
まずは、特別な権限のいらないProductサービスを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e160434d-9e0a-3ec5-0ae6-61bef9d79a14.png)
データが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/6e9a0e14-f596-2ab7-e9d6-16ea7a35474f.png)
次に、Adminサービスを実行してみます。こちらは権限なしエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d1445b60-6b42-7876-4024-0ef8edc6f48d.png)

####ユーザに権限を割り当て
Approuterサービスの中を見てみると、"admin"というロールができています。このロールをロールコレクションに割り当てたあと、自分のユーザに割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2ff11dd6-bcf6-31b5-e736-4f2119d7726e.png)
Approuterのアプリケーションから一旦ログアウトして再度ログオンします。今度はAdminサービスも照会できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5239a154-32d6-b47c-fcfb-ff27d025e98a.png)

ここまでで、Cloud Foundry上の認証と権限の動作が確認できました。

####ローカル実行用の設定
ローカル実行でもXSUAAを使用した認証が行われるようにします。

まず、ローカル実行時にsqliteのDBを見に行くようにpackage.jsonの設定を変更します。開発環境では[development]に指定した設定が優先されます。（※これは認証とは関係なく、単にローカル実行用の設定です）

```json:package.json
  "cds": {
    "requires": {
      "db": {
        "kind": "hana"
      },
      "uaa": {
        "kind": "xsuaa"
      }
    },
    "[development]": {
      "requires": {
        "db": {
          "kind": "sqlite"
        }
      }
    }
  }
```
次に、.cdsrc.jsonファイルで認証のstrategyを以下のように変更します。これで、モックの認証ではなくJWTトークンを使用した認証が行われるようになります。

```json:.cdsrc.json
{
    "auth": {
        "passport": {
            "strategy": "JWT"
        }
    }
}
```

approuter配下にdefault-env.jsonとdefault-services.jsonという2つのファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/44fc57d1-938e-60ad-236a-9914ec4e7ad4.png)

default-env.jsonでは、srv-bindingが示すローカルの宛先を指定します。

```json:approuter/default-env.json
{
	"destinations": [
		{
			"name": "srv-binding",
			"url": "http://localhost:4004",
			"forwardAuthToken": true
		}
	]
}
```
default-services.jsonには、XSUAAサービスインスタンスからコピーしたcredentailsの情報を貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e3ca9dcb-719b-642a-b375-f8993ae1aeca.png)

```json:approuter/default-services.json
{
    "uaa": {
        "tenantmode": "dedicated",
        "sburl": "https://internal-xsuaa.authentication.eu10.hana.ondemand.com",
        "clientid": "sb-products!t50054",
        "xsappname": "products!t50054",
        "clientsecret": "Hup0idYvmxnqSr3QXzR6bmq8j4c=",
        "url": "https://mio-trial.authentication.eu10.hana.ondemand.com",
        "uaadomain": "authentication.eu10.hana.ondemand.com",
        "verificationkey": "-----BEGIN PUBLIC KEY-----<public key>-----END PUBLIC KEY-----",
        "apiurl": "https://api.authentication.eu10.hana.ondemand.com",
        "identityzone": "mio-trial",
        "identityzoneid": "18954ba6-87b9-4059-ba9a-71bc85aa6889",
        "tenantid": "18954ba6-87b9-4059-ba9a-71bc85aa6889"            
    }
}
```
同じ内容を、プロジェクトルートに作成したdefault-services.jsonファイルにも設定します。このファイルは、ローカル実行の際にJWTトークンを検証するために使われます。先頭の"uaa"の部分は"xsuaa"に変更します。

```json:default-services.json
{
    "xsuaa": {
        "tenantmode": "dedicated",
        "sburl": "https://internal-xsuaa.authentication.eu10.hana.ondemand.com",
        "clientid": "sb-products!t50054",
        "xsappname": "products!t50054",
        "clientsecret": "Hup0idYvmxnqSr3QXzR6bmq8j4c=",
        "url": "https://mio-trial.authentication.eu10.hana.ondemand.com",
        "uaadomain": "authentication.eu10.hana.ondemand.com",
        "verificationkey": "-----BEGIN PUBLIC KEY-----<public key>-----END PUBLIC KEY-----",
        "apiurl": "https://api.authentication.eu10.hana.ondemand.com",
        "identityzone": "mio-trial",
        "identityzoneid": "18954ba6-87b9-4059-ba9a-71bc85aa6889",
        "tenantid": "18954ba6-87b9-4059-ba9a-71bc85aa6889"            
    }
}
```
以上の設定ができたらローカルで実行してみましょう。プロジェクトルートで`cds watch`を実行してサービスを立ち上げ、approuterフォルダに移動して`npm start`でApprouterを起動します。
Approuterはポート5000で実行されるので、ブラウザに`localhost:5000`と入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c863f338-eb8f-6a9b-f4e3-21a6b22a2207.png)
するとまず、認証画面にリダイレクトされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/193c3114-0268-4776-5ee5-de178d66ad5e.png)
Approuterのログを見ると、OAuthサーバにコードを要求しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/43ced003-cc32-689e-f5b0-1ecc78c30294.png)
認証されるとサービスにアクセスできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/da9d42dc-6dba-959a-363e-83d6758123c4.png)
権限が必要なAdminサービスも照会できます。（自分のユーザにロールコレクションを割り当てているため）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/425d3219-c853-1138-8b04-3874218e5ec7.png)
Approuterのログには認証されてからのトークンのやりとりについては表示されませんでした。

##認証されたユーザの情報を見てみる
せっかくなので、認証されたユーザの情報をログに表示させてみましょう。

srvフォルダの中にproduct-service.jsというファイルを作成します。以下はProductサービスのProductsエンティティに対するREADリクエストの前に呼ばれるハンドラです。リクエストコンテキストにはuserというオブジェクトがあり、ユーザに関する情報を持っています。

```javascript:srv/product-service.js
module.exports = function (){
  this.before ('READ','Products', (req)=>{
    console.log(req.user);
  })
}
```

ブラウザからProductsサービスを呼ぶと、コンソールにログインしているユーザの情報が表示されます。

```
GET /product/Products
{
  id: 'xxx@gmail.com',
  name: { givenName: 'Mio', familyName: 'Yasutake' },
  emails: [ { value: 'xxx@gmail.com' } ],
  valueOf: [Function],
  toString: [Function],
  is: [Function],
  has: [Function],
  locale: 'en'
}
```

この情報を使って、DBに書き込む前にユーザの情報で項目を埋めることも可能です。（以下は架空のエンティティです）

```javascript
  this.before ('CREATE','Orders', (req)=>{
    req.data.buyeremail = req.user.id
  })  
```

##おわりに
CAPで作成したサービスにモックの認証、XSUAAによる認証を追加する手順について見てきました。CAPではこのように段階的に認証の手段を変えることができます。本番に近い環境でテストできるのは便利ですね。

##参考
ブログ

- [CAP: Demystify User Authentication](https://blogs.sap.com/2020/07/20/cap-demystify-user-authentication/)
- [Demystifying XSUAA in SAP Cloud Foundry](https://blogs.sap.com/2020/08/20/demystifying-xsuaa-in-sap-cloud-foundry/)
- [Test your CAP nodejs applications with authentication locally](https://blogs.sap.com/2019/10/24/test-your-cap-nodejs-applications-with-authentication-locally/)


ドキュメント

- [CAP Node.js SDK Authentication](https://cap.cloud.sap/docs/node.js/authentication)
- [Authorization and Access Control](https://cap.cloud.sap/docs/guides/authorization#roles)
- [cloud-security-xsuaa-integration](https://github.com/SAP/cloud-security-xsuaa-integration)