こんにちは、白川です。
今回は、 ORY Hydra を使った OpenID Connect Provider の構築について紹介します。
マイクロサービスにおける認証・認可
モノリシックな Web アプリケーションでは、通常はそのアプリケーション自体がユーザ情報を保持し、ユーザの認証とセッションの生成を行ないます。
マイクロサービスを構成する各サービスが、モノリシックな Web アプリケーションと同じようにユーザ情報を保持すると、利用するユーザはサービス間を移動する際に別々にログインしないといけなくなります。
しかし、単純に各サービス間でユーザ情報やセッションをデータベースで共有する方法をとった場合、ユーザが認証されているかを知るために、すべてのサービスがデータベースにアクセスしないといけなくなってしまいますし、データベーススキーマ更新の際は全サービスに影響が出てしまいます。
そのため、マイクロサービスアーキテクチャでは各サービス共通で一度で認証し、各サービスで都度認証を行なわずにユーザーが認証済であることを確認できる疎結合な仕組みが必要となります。
このとき、従来の単純なセッションベースでの認証を採った場合、認証状態をセッションストアに都度問い合わせするような実装が多いです。
これを回避するためには、 OpenID Connect による JSON Web Token(JWT) 形式の ID トークンによる認証を採用するのが良さそうです。
OpenID Connect トークンベースの認証では、 OpenID Connect Provider によって JWT が生成され、その JWT はクライアントに返されます。
クライアントはリクエストごとに JWT をヘッダに含め、各サービスはリクエストヘッダの JWT の検証を行なうことで認証を行ないます。
ただし、 OpenID Connect の関連仕様は多い(参考リンク)ので、きちんとした OpenID Connect Provider を自前で実装するのはかなり大変そうです。
そこで、openid.netのLibraries, Products, and Toolsにあるライブラリを探したみたところ、 ORY Hydra を見つけました。
ORY Hydra の特徴
ORY Hydra は、 OpenID Foundation 公認の OpenID Connect Provider の Go 言語での実装です。
大きな特徴として、ORY Hydra 自体は独自の Identity Provider (IdP) を持たず、
ユーザ管理やログイン認証の部分に関しては、自前実装のものを API 呼出しする事で認証を行ないます。
これは既存のユーザ情報のデータストアを持っていて、そこに OpenID Connect のフローを組み込みたい場合に、非常に便利です。
以下 ORY Hydra の github にある README からの引用です。
ORY Hydra is not an identity provider (user sign up, user log in, password reset flow), but connects to your existing identity provider through a consent app.
(ORY Hydra は identity provider (ユーザーサインアップ、ログイン、パスワードリセット処理を行なう) ではありません。 しかし既存の identity provider に同意アプリケーションを通して接続します。)
本記事を執筆するにあたり、 ORY Hydra で OpenID Connect Provider を構築し、 kubernetes 上のアプリケーションで認証・認可するところまでを試してみました。
今回試したソースコードはこちらにアップしていますので、構築手順の詳細はそちらをご確認いただければと思います。
ログイン認証の実装に関しては、今回は公式に用意されたサンプルを利用しました。
ORY Hydra での OpenID Connect のフロー
公式ドキュメントから図を引用します。
認可エンドポイントへのアクセス → ログイン画面 → 同意画面表示 → トークン発行 というフローとなります。
図の中の Login Provider と Consent Provider がこちらで実装が必要なものとなります。
※引用: 公式ドキュメント
OpenID Connect Relying Party の登録
まず、OpenID Connect のクライアントである Relying Party を登録する必要があります。(上記シーケンス図の OAuth2 Client
に当たる部分です。)
今回はコマンドラインで登録します。
API も用意されていますが、これらは管理用 API を保護する手段は ORY Hydra では用意していないので、本番運用では外部からコールされないように別途保護する必要があります。
1 2 3 4 5 6 7 8 9 |
./hydra-darwin-amd64 clients create \ --skip-tls-verify --endpoint http://hydra-admin-api.synergy-example.com:30080 \ --id test-client \ --secret test-secret \ --response-types code,id_token \ --grant-types refresh_token,authorization_code \ --scope openid,offline \ --callbacks http://localhost:4446/callback |
Authorization Code Flow の開始
今回は RFC 6749 (The OAuth 2.0 Authorization Framework) で定義されている認可フローのうち, Authorization Code Flow (認可コードフロー) を試します。
Relying Party が ORY Hydra の認可エンドポイントにアクセスすることで、 Authorization Code Flow を開始します。
本来は Relying Party の実装も必要ですが、今回は Hydra が用意している ヘルパーコマンドラインを利用することで代用します。
1 2 3 4 5 6 7 8 9 |
./hydra-darwin-amd64 token user \ --skip-tls-verify \ --token-url http://hydra-public-api.synergy-example.com:30080/oauth2/token \ --auth-url http://hydra-public-api.synergy-example.com:30080/oauth2/auth \ --scope openid,offline \ --client-id test-client \ --client-secret test-secret \ --redirect http://localhost:4446/callback |
上記を実行すると、以下のように表示されて 認証完了のコールバックを受け取る Web サーバが立ち上がります。
1 2 3 4 5 6 7 |
Setting up home route on http://127.0.0.1:4446/ Setting up callback listener on http://127.0.0.1:4446/callback Press ctrl + c on Linux / Windows or cmd + c on OSX to end the process. If your browser does not open automatically, navigate to: http://127.0.0.1:4446/ |
http://127.0.0.1:4446 にアクセスすると、以下のような画面が表示されます。
Authorize Application のリンク先は以下のようになっています。 oauth2/auth
は ORY Hydra の認可エンドポイントです。
http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw
上記リンクを押下する前に、 シーケンス図における Login Provider と Consent Provider を起動させます。
今回は、公式に用意されている Node.js 製のサンプル実装を利用します。
1 2 3 4 |
cd /path/to/login-provider npm i NODE_TLS_REJECT_UNAUTHORIZED=0 HYDRA_ADMIN_URL=http://hydra-admin-api.synergy-example.com:30080 npm start |
Authorize Application リンクを押下すると、 ORY Hydra の認可エンドポイントに遷移します。
ORY Hydra は Cookie をチェックしてユーザの認証状態を判断して、 Login Provider にリダイレクトします。リダイレクト先のエンドポイントは通常 https://login-provider/login で表されます。この時、ORY Hydra から LoginProvider へのリダイレクトパスのクエリパラメータにlogin_challenge が付与されます。
ログイン
Login Provider はログイン画面を表示します。
サンプルではIDが "[email protected]"、パスワードが "foobar" 固定となっています。
ログイン画面にある "remember me" はチェックすることで、ログインリクエスト受理の際、 ORY Hydra に対して認証状態を保持するように依頼します。
これは再度認可エンドポイントにユーザがアクセスした場合に、再度認証をさせないためのものとして働きます。
正しいID/パスワードが入力された場合、Login Provider は login_challenge
をURLパスに含めて ORY Hydra のログインリクエスト受理エンドポイント にリクエストを送ります。
レスポンスには、ユーザが次にリダイレクトされるべきURLを含む redirect_to
キーが含まれています。
redirect_to
キー には以下のようなURLが指定されています。
http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&login_verifier=524e532413924f33a479738e9ebd756d&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw
この URL は ORY Hydra の認可エンドポイントですが、login_verifier
というパラメータが追加されていることが分かります。
リダイレクトされた ORY Hydra が、 login_verifier
を見てユーザが認証されているので次の同意フローに進んでよい、と判断します。
同意
次の同意フローに進んでよい、と判断した ORY Hydra は Consent Provider にリダイレクトします。
リダイレクト先のエンドポイントは通常 https://consent-provider/consent で表されます。この時クエリパラメータに consent_challenge
が付与されてきます。
Consent Provider は 同意画面を表示します。
ユーザが Relying Party に以前に権限を付与したことがない場合に、ユーザにその要求を確認させるために画面を表示します。
サンプルでは、openid
と offline
という2つのスコープに対する要求を確認しています。
openid
は ID トークンを発行するか、offline
はリフレッシュトークンを発行するか、という点に関わってきます。
Do not ask me again
は、同意した状態を ORY Hydra が保持しておくかどうかに関わります。
チェックした場合は、再度ログインした後に同意画面をスキップすることができます。
仕組みとしては、Consent Provider にリダイレクト後、 Consent Provider は consent_challenge を元に同意リクエストを ORY Hydra に確認する API をコールしますが、
以前に同意した場合は、この時のレスポンスの中に含まれる skip
キーがtrue
で返ってくるようになります。
Consent Provider は skip
キーを元に同意画面を出さずに、次の同意リクエスト受理に進むことができます。
ユーザの同意が得られると、
Consent Provider は consent_challenge
をURLパスに含めて ORY Hydra の同意リクエスト受理のエンドポイント にリクエストを送ります。
この同意リクエスト受理のリクエスト送信時に、session
キーに ID トークンに追加する任意の claim を指定することができます。
claim はOpenID Connect で定義された ID トークンに含まれるユーザー情報属性群です。
以下は、サンプルの Consent Provider の実装です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// Seems like the user authenticated! Let's tell hydra... hydra.getConsentRequest(challenge) // This will be called if the HTTP request was successful .then(function (response) { return hydra.acceptConsentRequest(challenge, { // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes // are requested accidentally. grant_scope: grant_scope, // The session allows us to set session data for id and access tokens session: { // This data will be available when introspecting the token. Try to avoid sensitive information here, // unless you limit who can introspect tokens. access_token: { groups: ['foo', 'bar'] }, // This data will be available in the ID token. ★これ id_token: { groups: ['foo', 'bar'] }, }, // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. grant_access_token_audience: response.requested_access_token_audience, // This tells hydra to remember this consent request and allow the same client to request the same // scopes from the same user, without showing the UI, in the future. remember: Boolean(req.body.remember), // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. // remember_for: 3600, remember_for: 0, }) .then(function (response) { // All we need to do now is to redirect the user back to hydra! console.log(response.redirect_to); res.redirect(response.redirect_to); }) }) // This will handle any error that happens when making HTTP calls to hydra .catch(function (error) { next(error); }); |
レスポンスには、ユーザが次にリダイレクトされるべきURLを含む redirect_to
キーが含まれています。
この URL は ORY Hydra の認可エンドポイントですが、consent_verifier
というパラメータが追加されていることが分かります。
http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&consent_verifier=8c33536a584d4443aa25ac226167785c&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw
トークン発行
認可エンドポイントに consent_verifier
パラメータ付きでリダイレクトされると、 Authorization Code (認可コード)を発行して、 Relying Party のコールバック URL にリダイレクトされます。
以下はサンプルにおけるコールバックURLです。
http://localhost:4446/callback?code=zRgk-QWsIOUKHsFTU1PREaY6WldH7rvjrtaa39yRQxM.9PcRUjQz437I7sT_2CoFsRnONjx2onZCx-8LKI36M98&scope=openid%20offline&state=ldhfjtraoocpxiyenhkvchxr
Authorization Code を受け取った Relying Party は state の検証を行なったのち、ORY Hydra のトークンエンドポイント に Authorization Code を Post して、 発行された ID トークン等を取得することができます。
( state はどのリクエストに対してどのレスポンスが帰ってきたか正しく対応づけされていることを保証することで CSRF 攻撃を防御するためのランダムな値です。)
サンプルでは以下のように発行されたアクセストークン、リフレッシュトークン、ID トークンが、画面に表示されました。
ID トークンを試しに jwt.io でデコードしてみると、ペイロード部分は以下のような感じになっていました。 ID トークンに追加する任意の claim として groups キーも確認することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "at_hash": "XsTiS1xujVf_MJgKihRJZQ", "aud": [ "test-client" ], "auth_time": 1553186222, "exp": 1553189938, "groups": [ "foo", "bar" ], "iat": 1553186338, "iss": "http://hydra-public-api.synergy-example.com:30080/", "jti": "75309150-0504-4be2-861d-c5db70360648", "nonce": "nfyfjvkmtqrdttmgfqkpaqcz", "rat": 1553186252, } |
ログアウト
ORY Hydra のログアウトのエンドポイント に GET でアクセスすることでログアウトできます。
ただし、ログアウトしても以前発行したアクセストークン等々は有効なままなので、トークンを取り消すエンドポイント の API もコールする必要があります。
トークンの無効化は、アクセストークンとリフレッシュトークンのみ有効です。 ID トークンの無効化はできず、 ID トークンの exp
キーが持つ有効期限までは有効となるため、上記とログアウトと連動しないという点は注意が必要です。
トークンを用いた kubernetes 上のアプリケーションでの認証・認可
kube-apiserver の認証・認可
ここからは 今回のサンプルで利用したコンテナオーケストレーションである kubernetes での OpenID Connect への対応について説明します。
kubernetes(kube-apiserver) の認証は OpenID Connect に対応しており、 RBAC(Role Based Access Control) を設定することでグループによるアクセス制御が可能です。
OpenID Connect による認証・認可に対応するために、 kube-apiserver 起動時にいくつかのパラメータを指定して起動する必要があります。
パラメータ名 | 指定する内容 |
---|---|
--oidc-issuer-url | IDトークンのiss ※ https必須 |
--oidc-client-id | Relying Partyのclient_id |
--oidc-groups-claim | RBACのGroupとして扱うclaimのキー |
--oidc-ca-file | IdPのサーバ証明書に署名したCA証明書 |
Docker for Mac の場合、 kube-apiserver は Moby VM 上でコンテナとして動作しているので、
こちらの方法で、 Docker for Mac の tty に接続して、
/etc/kubernetes/manifests/kube-apiserver.yaml
を下記のように編集した後に kubernetes を再起動します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
(中略) spec: containers: - command: - kube-apiserver - --authorization-mode=Node,RBAC (中略) - --oidc-issuer-url=https://hydra-public-api.synergy-example.com:30443/ - --oidc-client-id=test-client - --oidc-groups-claim=groups - --oidc-ca-file=/path/to/cafile (中略) - mountPath: /usr/share/ca-certificates name: usr-share-ca-certificates readOnly: true # 追記 - mountPath: /path/to/cafile name: oidc-ca-certificates readOnly: true hostNetwork: true (中略) - hostPath: path: /usr/share/ca-certificates type: DirectoryOrCreate name: usr-share-ca-certificates # 追記 - hostPath: path: /path/to/cafile type: DirectoryOrCreate name: oidc-ca-certificates status: {} |
次に、先ほど Hydra から発行された ID トークンを kubernetes に登録します。
1 2 3 |
kubectl config set-credentials synergy-admin --token=[YOUR ID TOKEN] kubectl config set-context oidc-example --cluster=docker-desktop --user=synergy-admin |
次に、 Role
と RoleBinding
を kubernetes に 登録します。登録内容としては namespace
が kube-system の pod に対する get/watch/list の操作を許可する内容となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: test-role namespace: kube-system rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "watch", "list"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: test-rolebinding namespace: kube-system subjects: - kind: Group name: foo # Name is case sensitive apiGroup: rbac.authorization.k8s.io roleRef: kind: Role #this must be Role or ClusterRole name: test-role # this must match the name of the Role or ClusterRole you wish to bind to apiGroup: rbac.authorization.k8s.io |
実際に試してみると、namespace 未指定の場合は pod の情報取得ができませんでしたが、 kube-system を指定した場合は pod の情報取得ができるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ kubectl config use-context oidc-example $ kubectl get pods Error from server (Forbidden): pods is forbidden: User "https://hydra-public-api.synergy-example.com:30443/#[email protected]" cannot list resource "pods" in API group "" in the namespace "default" $ kubectl get pod -n kube-system NAME READY STATUS RESTARTS AGE coredns-86c58d9df4-9sdb9 1/1 Running 7 66d coredns-86c58d9df4-f288f 1/1 Running 7 66d etcd-docker-desktop 1/1 Running 25 66d kube-apiserver-docker-desktop 1/1 Running 0 2m45s kube-controller-manager-docker-desktop 1/1 Running 82 66d kube-proxy-nswq7 1/1 Running 7 66d kube-scheduler-docker-desktop 1/1 Running 75 66d tiller-deploy-dbb85cb99-bjrnr 1/1 Running 7 3d8h |
istio による認証・認可
istio はマイクロサービスのサービスメッシュを実現するオープンソースプロジェクトです。
kubernetes の pod 内に Envoy というプロキシをサイドカーとして動かすことで、アプリケーションのコードに手を入れずに、マイクロサービス間の通信に関する課題(リトライ、サーキットブレーカー、負荷分散、分散トレーシング)を解決するための仕組みを提供します。
先ほど見た kube-apiserver の例はあくまで kube-apiserver に対する操作の認証・認可ですが、
istio によって、OpenID Connect による認証・認可を各マイクロサービスのアプリケーションや HTTP メソッド単位で実施することも可能になります。
サンプル では、 httpbin のサービスを立てて、認証・認可のポリシーを設定しています。
認証に関しては、Policy
を kubernetes リソースとして登録します。
targets
に対象のサービス、origins
に OpenID Connect の issuer と JWKsエンドポイントを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
apiVersion: authentication.istio.io/v1alpha1 kind: Policy metadata: name: jwt-example namespace: foo spec: targets: - name: httpbin origins: - jwt: issuer: http://hydra-public-api.synergy-example.com:30080/ jwksUri: http://hydra-public-api.synergy-example.com:30080/.well-known/jwks.json principalBinding: USE_ORIGIN |
認可については、RbacConfig
、ServiceRole
、ServiceRoleBinding
を kubernetes リソースとして登録します。
以下のサンプルでは、JWT にペイロードに指定された groups クレームが foo
のユーザに対して、namespace が foo
の全サービス、全メソッドを許可しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
apiVersion: rbac.istio.io/v1alpha1 kind: RbacConfig metadata: name: default namespace: istio-system spec: mode: ON_WITH_INCLUSION inclusion: namespaces: - foo --- apiVersion: rbac.istio.io/v1alpha1 kind: ServiceRole metadata: name: trusted-visitor namespace: foo spec: rules: - methods: - '*' services: - '*' --- apiVersion: rbac.istio.io/v1alpha1 kind: ServiceRoleBinding metadata: name: jwt-binding namespace: foo spec: roleRef: kind: ServiceRole name: trusted-visitor subjects: - properties: request.auth.claims[groups]: "foo" |
では、実際にアクセスしてみて試してみます。
Bearer トークンで ID トークン指定なしだと API コールできず、正しい ID トークンを指定した場合は API コールが通ることが確認できました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# ID トークン指定なしのアクセス $ curl -i http://localhost/status/200 # 出力結果 HTTP/1.1 401 Unauthorized content-length: 29 content-type: text/plain date: Thu, 21 Mar 2019 19:29:10 GMT server: istio-envoy x-envoy-upstream-service-time: 0 Origin authentication failed. --- # ID トークン指定したアクセス $ curl -i -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzo0MDU1N2YzNC1jZGRjLTRmMjItYmY3Yi0zZWI4OWJkOGZmMTEiLCJ0eXAiOiJKV1QifQ.eyJhdF9oYXNoIjoiSjMwYkJEYkpLNHdFdzBodkdLRWRLQSIsImF1ZCI6WyJ0ZXN0LWNsaWVudCJdLCJhdXRoX3RpbWUiOjE1NTMxOTYzNzUsImV4cCI6MTU1MzE5OTk4MSwiZ3JvdXBzIjpbImZvbyIsImJhciJdLCJpYXQiOjE1NTMxOTYzODEsImlzcyI6Imh0dHA6Ly9oeWRyYS1wdWJsaWMtYXBpLnN5bmVyZ3ktZXhhbXBsZS5jb206MzAwODAvIiwianRpIjoiZGYyOGYzMmMtZWE0ZS00N2NlLWJlZWItYTQ5MjFmNjVmMmQ3Iiwibm9uY2UiOiJpeHBsdWV2ZGZyeXJ1emptbnhrdnd1ZWgiLCJyYXQiOjE1NTMxOTYzNjQsInN1YiI6ImZvb0BiYXIuY29tIn0.EK4h5_dnbXIe-WdC5VmEfrD7AXIw2i1bmLBXMbssJD70TFlhkYQ4oUTZBQiUQJwsLGMufPzpUqvI16r_UHSl9oR4dfDljDO08sr_WhCy5HxVHg-uX8-ZZ2t-E1cb6_Jj8Rk7NACefuRWpCE7tU-jKsanI_Yq4etCUn9voPM6mwM3ga9A9bmr1qyCykaE-EFraqAJ3JC0X6b8Rfiho62hM5vxFxhU6SAFHfqj06vyeqi7vk925loiL0XoY4XD0D0Bf8m_TcJNbwVb4sOphwHOIL280qDCpr4AFemLTWsujNCV2v0X8_LryDVVBex_FPC5q5uOVTBWRqhPONYTBCjJj8f7zdAnYqUhSQah-43BkvLUaXtWiMk4EwgRsMqxo7rVOKYF88nlIThZ3eWxut501WKZSYzUN6EDeCJCU-g0E-Cy18Ht0vToWmAKo5KXpvypwEYit8HLpTIqorYmlwXtuzHE_LOGZNSNIvaNg9YieL53Afmx7HI3E_HsDZ8AO-pXPqmA35-6FMZ-wCOLMbwWuulsOS1VcCXM0G86EsHMMbSMUtm9hnHww9ZLAZMOw-jqgIGYpcetPnJww3UqvjwxdW5W-cqlU3wQpb3d-3Dls34nRaoqLie8lkvOjkJl2A8WTf5GNT2ncTfo5LQGpvWPrG1ucf8du8iEjqblhQx9Aiw' http://localhost/status/200 # 出力結果 HTTP/1.1 200 OK server: istio-envoy date: Thu, 21 Mar 2019 19:37:49 GMT content-type: text/html; charset=utf-8 access-control-allow-origin: * access-control-allow-credentials: true content-length: 0 x-envoy-upstream-service-time: 7 |
おわりに
実運用に入る前には、今回のサンプル実装にくわえて API の保護であったり、 ID トークンの署名に使用する鍵のローテーションの考慮のなどを行なう必要があります。
とはいえ、 ORY Hydra を利用することで、独自のログイン認証を行ないつつ、 OpenID Connect に対応して、 kubernetes や istio での認証・認可に対応できることが分かりました。
実装コストを抑えながら、既存のユーザストアや認証処理を OpenID Connect に対応させる場合などに、非常に魅力的な選択であることが分かりました。
最後に参考文献を載せておきます。