##はじめに
以前の記事で、CAPのHello World的なサービスを作成しました。
このサービスは"誰でも"使える状態でしたが、アクセスできるユーザを絞るようにしたいです。
【CAP】CAPで作ったODataをUI5アプリで使う(1) CAPサービスの作成
そこで、この記事では以下のテーマについて扱います。
・CAPで作ったODataサービスをCloud Platormで認証されたユーザのみ使用可能にする
・認証の設定をしたうえでローカルでも実行できるようにする
##Cloud Platformでのユーザ認証の仕組み
Cloud Platformで動くアプリケーションでは、ApprouterとXSUAAサービスのコンビネーションにより認証が行われます。以下のページの説明がわかりやすかったので翻訳・引用します。
https://github.com/SAP/cloud-security-xsuaa-integration
- ユーザがWebアプリケーションにブラウザやモバイルデバイスを介してアクセスする
- Webアプリケーション(典型的なSAP Cloud Platformアプリケーションの場合、Application Router)はOAuthクライアントとして、認証のためにOAuthサーバにリダイレクトする ⇒ユーザはID、パスワードの入力を求められる
- 認証されるとWebアプリケーションはOAuthサーバから発行されたコードを使ってアクセストークンを要求する
- Webアプリケーションはアクセストークンを使ってOAuthリソースサーバからデータを取得する。OAuthリソースサーバはオンラインまたはオフラインでトークンを検証する
アクセストークンは**JWT(Json Web Token)**とも呼ばれます。ユーザID、氏名などのほか、ユーザが持つ権限(スコープ)が記載されています。
##CAPプロジェクトに認証を追加
以下の記事で作成したCAPプロジェクトに認証を追加します。
【CAP】CAPで作ったODataをUI5アプリで使う(1) CAPサービスの作成
<ステップ>
- アクセスコントロール用のアノテーションを追加
- モックの認証を設定(ローカル実行用)
- トークンを使用した認証を設定(ローカル、本番実行用)
###1. アクセスコントロール用のアノテーションを追加
もともとあったproduct-service.cdsに加え、admin-service.cdsを作成します。
Productサービスは認証されたユーザのみ実行可能、Adminサービスは"admin"ロールを持つユーザのみ実行可能とします。
アクセスコントロールの設定方法については以下のドキュメントをご参照ください。
https://cap.cloud.sap/docs/guides/authorization#roles
using demo.products as prod from '../db/schema';
@requires: 'authenticated-user'
service ProductService {
entity Products as projection on prod.Products;
}
using demo.products as prod from '../db/schema';
@requires: 'admin'
service AdminService {
entity Products as projection on prod.Products;
}
cds watch
で実行してみます。まずProductサービスのProductsエンティティを見てみます。
認証を求められますが、何も入力せずにサインインしてもデータが表示されます。
次に、AdminサービスのProductsエンティティを見てみます。こちらは権限がないのでエラーになりました。
<ここまででわかったこと>
ローカル実行の場合、
・authenticated-userが実行できるサービスは、サインインした後は誰でも使える
・特定のロールのみ実行可能なサービスは、そのロールを持つユーザでないと使えない
###2. モックの認証を設定
ローカルでもロールを持ったユーザとして実行できるように、モックの認証の設定をします。
まず、passportをプロジェクトに追加します。
npm add passport
続いて、.cdsrc.jsonファイルを以下のように設定します。これで、adminロールを持つ"admin"というユーザを作成したことになります。(.cdsrc.jsonはプロジェクトのルートに置きます)
{
"auth": {
"passport": {
"strategy": "mock",
"users": {
"admin": {
"password": "dummy",
"ID": "admin",
"roles": ["admin", "authenticated-user"]
}
}
}
}
}
再度実行してみましょう。前回のログイン情報が影響しないようにシークレットウインドウで開き、adminユーザでログインします。
AdminサービスのProductsエンティティが表示できました。
###3. トークンを使用した認証を設定
CAPプロジェクトにXSUAAの設定を追加します。最初に本番用の設定をしてCloud Foundryにデプロイしてから、ローカル実行用の設定をします。
####本番用の設定
まず、 @sap/xssecと@sap/xsenvという2つのモジュールをインストールします。
※以下のブログによると@sap/xssecのバージョン3はまだサポートされていないのでバージョン2を指定したほうがよさそうです
CAP: Demystify User Authentication
npm install @sap/xssec@ @sap/xsenv
XSUAA設定用のファイルを生成します。
cds compile srv/ --to xsuaa > xs-security.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セクションを以下のように設定します。
"cds": {
"requires": {
"db": {
"kind": "hana"
},
"uaa": {
"kind": "xsuaa"
}
}
}
mta.yamlにXSUAAの設定を追加します。全体は以下のようになります。
## 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サービスのインスタンスが登録されます。
Cloud Fondry環境でサービスを実行するとどうなるか試してみます。
特別な権限のいらないProductサービスの方を実行してみます。
Unauthorized、つまり「認証されていませんよ」というエラーになりました。
認証のための入り口が必要ということで、Approuterを追加します。
####Approuterを追加
プロジェクトのルートにapprouterフォルダを追加します。approuterの中に2つのファイル:package.jsonとxs-app.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のモジュールを追加します。
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として使用します。
xs-app.jsonを以下のように設定します。
Approuterにリクエストが入ってきたらsrv-bindingという宛先に流すという意味です。authenticationMethodが"route"となっているので、デフォルトでXSUAAを使用した認証が行われます。
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/(.*)",
"destination": "srv-binding"
}
]
}
ここまで設定できたら再度ビルド&デプロイします。
Approuterのアプリケーションを実行します。
ログインを求められるので、Cloud PlatformのユーザIDとパスワードを入力します。
まずは、特別な権限のいらないProductサービスを実行します。
データが表示されました。
次に、Adminサービスを実行してみます。こちらは権限なしエラーになります。
####ユーザに権限を割り当て
Approuterサービスの中を見てみると、"admin"というロールができています。このロールをロールコレクションに割り当てたあと、自分のユーザに割り当てます。
Approuterのアプリケーションから一旦ログアウトして再度ログオンします。今度はAdminサービスも照会できました。
ここまでで、Cloud Foundry上の認証と権限の動作が確認できました。
####ローカル実行用の設定
ローカル実行でもXSUAAを使用した認証が行われるようにします。
まず、ローカル実行時にsqliteのDBを見に行くようにpackage.jsonの設定を変更します。開発環境では[development]に指定した設定が優先されます。(※これは認証とは関係なく、単にローカル実行用の設定です)
"cds": {
"requires": {
"db": {
"kind": "hana"
},
"uaa": {
"kind": "xsuaa"
}
},
"[development]": {
"requires": {
"db": {
"kind": "sqlite"
}
}
}
}
次に、.cdsrc.jsonファイルで認証のstrategyを以下のように変更します。これで、モックの認証ではなくJWTトークンを使用した認証が行われるようになります。
{
"auth": {
"passport": {
"strategy": "JWT"
}
}
}
approuter配下にdefault-env.jsonとdefault-services.jsonという2つのファイルを作成します。
default-env.jsonでは、srv-bindingが示すローカルの宛先を指定します。
{
"destinations": [
{
"name": "srv-binding",
"url": "http://localhost:4004",
"forwardAuthToken": true
}
]
}
default-services.jsonには、XSUAAサービスインスタンスからコピーしたcredentailsの情報を貼り付けます。
{
"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"に変更します。
{
"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
と入力します。
するとまず、認証画面にリダイレクトされます。
Approuterのログを見ると、OAuthサーバにコードを要求しています。
認証されるとサービスにアクセスできます。
権限が必要なAdminサービスも照会できます。(自分のユーザにロールコレクションを割り当てているため)
Approuterのログには認証されてからのトークンのやりとりについては表示されませんでした。
##認証されたユーザの情報を見てみる
せっかくなので、認証されたユーザの情報をログに表示させてみましょう。
srvフォルダの中にproduct-service.jsというファイルを作成します。以下はProductサービスのProductsエンティティに対するREADリクエストの前に呼ばれるハンドラです。リクエストコンテキストにはuserというオブジェクトがあり、ユーザに関する情報を持っています。
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に書き込む前にユーザの情報で項目を埋めることも可能です。(以下は架空のエンティティです)
this.before ('CREATE','Orders', (req)=>{
req.data.buyeremail = req.user.id
})
##おわりに
CAPで作成したサービスにモックの認証、XSUAAによる認証を追加する手順について見てきました。CAPではこのように段階的に認証の手段を変えることができます。本番に近い環境でテストできるのは便利ですね。
##参考
ブログ
- CAP: Demystify User Authentication
- Demystifying XSUAA in SAP Cloud Foundry
- Test your CAP nodejs applications with authentication locally
ドキュメント