0 フォロワー · 22 投稿

OAuth2の認証に関する記事と質問。

記事 Toshihiko Minamoto · 5月 16, 2025 5m read

Auth0 と InterSystems IRIS FHIR リポジトリ使った SMART On FHIR に関する連載最終回では、Angular 16 で開発したアプリケーションをレビューします。

このソリューションに定義されたアーキテクチャがどのように構成されているかを思い出しましょう。

フロントエンドのアプリケーションは 2 列目で、ご覧のように 2 つのことを行います。

  1. ログインリクエストを Auth0 にリダイレクトし、レスポンスを受信する。
  2. REST 経由でリクエストを FHIR サーバーに送信し、そのレスポンスを受信する。

Angular

Angular は TypeScript が開発し、Google が管理するオープンソースの Web アプリケーションフレームワークで、シングルページ Web アプリケーションの作成と管理に使用されます。 この「シングルページアプリケーション」デザイン手法によって、はるかに動的なユーザー向けアプリケーションを設計することができます。 最初の記事で説明したとおり、ここではアプリケーションサーバーとリバースプロキシとして NGINX を使用し、呼び出しヘッダーがサーバーの呼び出しヘッダーに一致するように変更して、CORS から派生する問題を回避します。

アプリケーションのデザイン

モバイルアプリケーションのデザインをシミュレートするように、Angular Material を使ってアプリケーションを設計しました。 この例では、アプリケーションは心拍数、血圧、および体重などの一連の患者データを記録することを目的としており、このために、2 種類の FHIR リソースをサーバーに送信します。1 つ目はユーザーがデータを登録する Patient タイプリソースで、2 つ目は、送信しようとしている各タイプのデータを送信する Observation リソースに対応します。

アプリケーションには、記録されたデータの変化がグラフで表示されます。

ログイン画面

ユーザーがパス https:\\localhost にアクセスすると最初の画面が表示され、そこからログインをリクエストできます。

 

ログインボタンをクリックすると、アプリケーションは構成済みの API に対して有効化された Auth0 ページに自動的にユーザーをリダイレクトします。

ユーザー名とパスワードを入力すると、Auth0 はデータへのアクセス許可をアプリケーションに付与するよう求めます。 データへのアクセスが確認されたら、Auth0 は、構成プロセス中に指定した URL にリダイレクトします。 アクセストークンが生成されると、Auth0 ライブラリは、サーバーに対して発行するすべての呼び出しのヘッダーにそのトークンを含めるようになります。 これは以下の図で確認できます。

最初の画面

ログインが完了すると、ログインユーザーが使用できる情報を FHIR サーバーにリクエストする最初の通信が発生します。これには、パラメーターによるクエリを使用して、次のような GET 呼び出しを送信します。

https://localhost:8443/smart/fhir/r5/Patient?email=lperezra%40intersystems.com

サーバーのレスポンスは次の情報を含む Bundle タイプリソースです。

{
    "resourceType":"Bundle",
    "id":"8c5b1efd-cfdd-11ee-a06b-0242ac190002",
    "type":"searchset",
    "timestamp":"2024-02-20T10:48:14Z",
    "total":0,
    "link":[
        {
            "relation":"self",
            "url":"https://localhost:8443/smart/fhir/r5/Patient?email=lperezra%40intersystems.com"
        }
    ]
}

ご覧のように、そのメールアドレスを使用する患者は合計 0 人であるため、アプリケーションにはデータを登録できる最初の画面が表示されます。

 

ご覧のように、メールアドレスのフィールドにはログインユーザーのアドレスが入力されています。これは、最初のクエリで見たように、メールアドレスを ID として使用するためです。 フォームの入力が完了したら、POST 経由で次のような呼び出しを送信します。

https://localhost:8443/smart/fhir/r5/Patient

Patient リソースによって形成されたメッセージ本文:

{
    "resourceType":"Patient",
    "birthDate":"1982-03-08",
    "gender":"male",
    "identifier":[
        {
            "type":{
                "text":"ID"
            },
            "value":"12345678A"
        }
    ],
    "name":[
        {
            "family":"PÉREZ RAMOS",
            "given":[
                "LUIS ÁNGEL"
                ]
        }
    ],
    "telecom":[
        {
            "system":"phone",
            "value":"600102030"
        },
        {
            "system":"email",
            "value":"lperezra@intersystems.com"
        }
    ]
}

患者データがサーバーに登録され、患者のクエリによって結果が返されるようになったため、さまざまな経過観察を記録できる準備が整いました。 最初の画面がどのように表示されるか見てみましょう。

経過観察画面

患者のデータを送信したのと同じ方法で、特定の画面から経過観察を送信します。

サーバーに送信されたリソースごとに、アプリケーションは新しい点をグラフに追加します。

これを行うために、そのユーザーに属する Observation タイプリソースをリクエストするクエリをサーバーに発行します。

https://localhost/smart/fhir/r5/Observation?patient=Patient/1

すると、サーバーはもう一度、その患者に対して記録されたすべての経過観察を含む Bundle タイプリソースを返します。

結果が取得されたので、アプリケーションはすべての数値を抽出し、関連するグラフを構築します。

まとめ

この記事と前回の 2 つの記事で確認したように、SMART On FHIR アプリケーションの設計と作成はそれほど複雑ではなく、FHIR サーバーで使用できるすべての機能を利用するアプリケーションを素早くアジャイルに構築することができます。

この種のアプリケーションでは、データに対する CRUD タイプの操作を管理する複雑なバックエンドの開発が不要であり、OAuth2 を使用することで、アプリケーションのユーザーを管理する必要もありません。その機能は Auth0 または選択した認証・承認サーバーに任せることができます。

SMART On FHIR では、簡単かつ単純な方法で、患者と医療専門家に対し医療データ管理に必要なツールを提供することができます。

お読みいただきありがとうございました!

0
0 40
記事 Seisuke Nakahashi · 5月 31, 2022 6m read

2022年5月30日より、外部アプリからGmailを送信するには OAuth 2.0が必須になりました (リンク) そのため、IRIS から Gmail を送信したい 場合は、今後は OAuth 2.0 を利用して Gmailアカウントに接続する 必要があります。 そこで、IRIS コード ObjectScript を使って、OAuth 2.0 でGmailアカウントに接続&メール送信するサンプルを作成しました。 ぜひご活用ください!

2
0 1433
記事 Tomohiro Iwamoto · 11月 13, 2024 7m read

以前、Azure用にOAouth2クライアントをセットアップする記事を書いた時に思ったのですが、各IdPはサンプルコードとしてPythonコードや専用のモジュールを提供しているので、それがそのまま使用できれば効率が良いのにな、と思いました。

IRISが埋め込みPython機能としてWSGIをサポートしたことにより、これが簡単に実現しそうなので、その方法をご紹介したいと思います。

導入方法

今回は、IdPとしてOKTAを使用してAuthorization Codeフローを行います。

OKTAでの設定内容

参考までに、今回使用した環境を後半に記載しています。

アプリケーションの起動

コンテナ化してありますので、コンテナのビルド環境をお持ちの方は、下記を実行してください。

git clone https://github.com/IRISMeister/iris-okta-oidc-wsgi
cd iris-okta-oidc-wsgi

python/.env.templateをpython/.envという名前でコピーを作成して、OKTAで得られる設定値を指定してください。

AUTH0_CLIENT_ID="0oaxxxxxxx"  
AUTH0_CLIENT_SECRET="qUudxxxxxxxxxxx"
AUTH0_DOMAIN="dev-xxxxx.okta.com/oauth2/default"

AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRETは後述の「アプリケーション追加」で使用する画面から取得できます。 AUTH0_DOMAINは、後述の「カスタムSCOPE追加」で使用する画面から取得できる発行者URIを設定します。

docker compose build
docker compose up -d

下記でIRISの管理ポータルにアクセスできます。

http://localhost:8882/csp/sys/%25CSP.Portal.Home.zen
ユーザ名:SuperUser, パスワード:SYS

WSGI環境での実行

まずは、純粋なWSGI環境での実行を行って、設定が正しくできているかを確認します。コードはこちらを使用しました。

元々はAuth0用ですが、ほぼそのままでOKTAでも使用できました

下記のコマンドでFlaskを起動します。

docker compose exec iris python3 /usr/irissys/mgr/python/run.py

ブラウザでメインページにアクセスしてください。

http://127.0.0.1:8889/ ではリダイレクトに失敗します。

「Login」をクリックするとOKTAのログイン画面が表示されます。OKTAサインアップ時に使用した多要素認証(スマホアプリ)を使用してログインしてください。

うまく動作した場合は、取得したトークンの情報等が表示されます。namespace: USERと表示されている通り、IRISへのアクセスも行っています。

Welcome Tomohiro Iwamoto!
Logout

{
    "access_token": "eyJraWQiOi.....",
    "expires_at": 1731482958,
    "expires_in": 3600,
    "id_token": "eyJraWQ......",
    "scope": "email user/*.* profile openid",
    "token_type": "Bearer",
    "userinfo": {
        "amr": [
            "mfa",
            "otp",
            "pwd"
        ],
        "at_hash": "3cRg3plSvDPqGUwEBzefoA",
        "aud": "xxxxx",
        "auth_time": 1731477799,
        "email": "iwamoto@intersystems.com",
        "exp": 1731482958,
        "iat": 1731479358,
        "idp": "xxxxxxxxxx",
        "iss": "https://dev-xxxxxx.okta.com/oauth2/default",
        "jti": "ID.Z0icZKkP61n3WDLgD08q3QxJ4Ags6_rwhrqFX3lAUjs",
        "name": "Tomohiro Iwamoto",
        "nonce": "DYrD0GKQPyXuT6Fni1So",
        "preferred_username": "iwamoto@intersystems.com",
        "sub": "xxxxxx",
        "ver": 1
    }
}
namespace: USER

「Logout」をクリックするとOKTAからのログアウトが実行され、最初のページにリダイレクトされます。

IRIS+WSGI環境での実行

ブラウザでIRIS+WSGI用のメインページにアクセスしてください。以降の操作は同じです。

同じFlask用のコードを使用していますので、全く同じ動作になります。

何が可能になったのか

これは「IRIS+WSGIで何が可能になるか?」という問いと同じですが、本トピックに限定すると、例えばbearer tokenであるアクセストークンを、cookie(flaskのsessionの仕組み)ではなく、IRISのDB上に保存する、他のCSP/RESTアプリケーションに渡す、という応用が考えられます。

元々、CSPやIRISのRESTで作成したアプリケーションがあって、そこにIdP発行のアクセストークンを渡したい、といった用法に向いているアプローチかと思います。

また、IRISでWSGIを実行することにより、gunicornのような運用レベルのWSGI用ウェブサーバを別途立てる必要がなくなります。

OKTAでの設定内容

今回使用した環境です。

こちらのトライアル環境を使用しました。若干画面が変わっていましたが、サインアップ手順はこちらを参考にしました。登録の際にはMFA(多要素認証)としてスマホが必要です。

ログインすると、次のようなメイン画面が表示されます。

以降、アプリケーション追加、カスタムSCOPE追加、認証ポリシー設定などを行っています。

アプリケーション追加

メニューのアプリケーション->アプリケーションで、flask-code-flowという名称でアプリケーションを追加します。

「一般」タブのクライアント資格情報のは下記のようになります。

一般設定は下記のようになります。

ログイン設定は下記のようになります。複数の登録がありますが、これは実行環境に合わせて各々オリジン(ブラウザで指定するURL)が異なるためです。

ログアウト設定は下記のようになります。複数の登録がある理由はログイン設定と同じです。

「サインオン」タブの設定は下記のようになります。サインオン方法としてOpenID Connectが指定されます。

「サインオン」タブの下のほうに「ユーザー認証」というセクションがありますので、そこのポリシーの詳細リンクを押して「認証ポリシー」画面に遷移します。

「認証ポリシー」では、ルールはそのままで、アプリケーションには追加したアプリケーションを「アプリを追加」で追加登録します。

「割り当て」タブのクライアント資格情報のは下記のようになります。

カスタムSCOPE追加

リソースサーバ用にカスタムのSCOPEを追加します。「セキュリティ」->「API」でdefaultを編集して、オーディエンスとして適当なURL(ここでは http://localhost/csp/healthshare/sc/fhir/r4 )を設定します。

defaultをクリックすると下記の画面に遷移します。

「スコープ」タブを選択し、「スコープを追加」を押してカスタムSCOPEを追加します。ここではuser/*.*というスコープを追加しました。

アクセスポリシー設定

アクセスポリシーを追加します。「セキュリティ」->「API」で認可サーバ:defaultを選択します。「アクセスポリシー」タブを選択し、default policyにアプリケーションを追加します。

「ルールを追加」を押して、新しいルールを追加します。以下のような設定にしました。

0
0 68
記事 Toshihiko Minamoto · 8月 6, 2024 7m read

前回の記事でSMART On FHIRプロジェクトのアーキテクチャを紹介したので、いよいよ本題に入り、必要となる全ての要素の設定を始めましょう。

まずはAuth0から始めます。

Auth0の設定

登録が完了したら、左側のメニューから最初のアプリケーションを作成します

Application menu

この例では、Angular 16で開発されたアプリケーションなので、Single Page Web Applicationタイプとなります。このオプションを選択し、Createをクリックします。

Single Page Web Application

次の画面では以下のフィールドを定義する必要があります。

注意: URLはすべてHTTPSでなければなりません。これはOAuth2接続要件の1つです。

これはOAuth2接続の要件の1つです。これでAuth0が認証と認可プロセスの後にユーザーをリダイレクトするために必要なURLを設定しました。このURLにはポートが定義されていませんが、これはNGINXを通してAngularプロジェクトをDockerにデプロイする際に、デフォルトのHTTPSポート443からアクセスするように指定したためです。好きな名前を付けてください。

Application configuration

この後のAngularプロジェクトの設定について、DomainClient IDの両方にある値を書き出します。

アプリケーションの設定が完了したら、次はAngularアプリケーションからのリクエストを受け取るAPIを定義します。

このオプションを選択すると、必要なデータを入力する画面が表示されます。

API configuration

後でAngularアプリケーションが適切に接続するための "環境 "として使用するAPIの識別子を定義する必要があります。見ての通り、URLを入力することが推奨されていますが、このURLは識別子として使用されるだけなので、実際に機能する必要はありません。この場合、以下のように定義できます。

https://localhost/smart/fhir/r5

最後に、署名アルゴリズムをRS256に設定し、接続ユーザーのFHIRのスコープを定義するPermissionsタブに進みます

API permission

FHIRコンテキストのトピックをより深く知りたい場合は、[ここ](http://hl7.org/fhir/smart-app-launch/1.0.0/scopes-and-launch-context/index.html)をクリックして公式ページのURLを参照してください。この例では、user/*.*スコープを定義し、認証されたユーザーがすべての FHIR サーバーリソースに対して CRUD 操作を実行できるようにしています。

完璧です!Auth0アカウントがOAuth2サーバとして機能するように設定できました。

Angularアプリの設定

このアプリケーションはAngular 17で開発したかったのですが、残念ながらAuth0とそのライブラリの関連ドキュメントはAngular 16のものしかないので、簡単な方を選ぶこととしバージョン16で開発しました。

Angularプロジェクトを設定するには、app.module.tsページを開き、以下のコードを探します

設定する各パラメータの意味を見てみましょう。

  • domain:Auth0 でアプリケーションを作成したときに生成された ドメイン値
  • clientId: 上と同じく生成されたClient ID
  • audience: APIの識別子として設定したURL
  • uri: Auth0 TypeScriptライブラリに、そのURIを含むURLへのすべての呼び出しをインターセプトし、Auth0が検証時に返すAccess_tokenを組み込むように指示します(呼び出しのヘッダにAuthorizationパラメータを追加します: Bearer...)

これらの値を変更すると、AngularアプリケーションがAuth0で動作するように設定されます。次の記事では、ユーザーインターフェースからAuth0を呼び出す方法を詳しく説明します。

InterSystems IRIS for Healthの設定

このプロジェクトは、デプロイプロセス中にFHIRサーバーを自動的にインストールするように設定されているため、手順を省くことができます。今回のケースでは、FHIRサーバーのエンドポイントとして/smart/fhir/r5というURIを定義しました。FHIRの用語に馴染みのない方のために説明すると、r5とはFHIRの最新バージョンのことで、IRISで数ヶ月前から利用可能です。

IRISインスタンスを設定するには、まずDockerを起動し、プロジェクトで利用可能な3つのコンテナをデプロイする必要があります。プロジェクトのルートからターミナルで以下のコマンドを実行するだけです。

docker-compose up -d --build

これで、プロジェクトに存在するコンテナをビルドして実行できるようになります。Windowsユーザーの場合、Docker Desktopを使えば、このような画面が表示されるはずです。

Docker Desktop

ここでは3つのコンテナがあります。

  • iris: FHIRサーバが配置されているIRISインスタンス
  • smart-ui: NGINXからデプロイされたウェブアプリケーションのコードで、すべての接続が関連する証明書とSSLを経由するよう設定されています。
  • webgateway: 関連するApacheサーバー(正式版2023.1以降、プライベートWebサーバーは廃止されましたが、コミュニティ版ではまだ利用可能であることを覚えておいてください)。

ウェブゲートウェイ

繰り返しになりますが、OAuth2をFHIRサーバーで使用するためには、すべての接続がHTTPS経由で行われることが必須であるため、Apacheサーバーはそのタイプの呼び出しのみを受け付けるように設定する必要があります。 webgateway/shared/CSP.confファイルをみると、Apacheサーバの設定を担当する以下のセクションがあります

# SSL SECTION #
# Enable SSL/TLS (https://) on the Apache web server.
# The user is responsible for providing valid SSL certificates.
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
LoadModule headers_module /usr/lib/apache2/modules/mod_headers.so
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile "/webgateway-shared/apache_webgateway.cer"
SSLCertificateKeyFile "/webgateway-shared/apache_webgateway.key"
Header add ACCESS-CONTROL-ALLOW-ORIGIN "*"
</VirtualHost>

443番ポートを経由するようにし、WebGatewayのURLはhttps://webgateway、アプリケーションからFHIRサーバへの呼び出しはこのURLにリダイレクトする必要があります。(ウェブゲートウェイは、Dockerコンテナによって作成されたネットワークに与えられたマスクです)

Angularからサーバーへのすべての呼び出しはURLhttps://localhost/smart/fhir/r5で行われ、NGINXはそのlocalhostをwebgatewayにリダイレクトする役割を果たします。smart-ui/nginx.confを開くと、以下のような設定になっています。

 

この設定では、Webアプリケーションは443番ポートをリッスンし、URLに/を含むすべての呼び出しはAngularアプリケーションによって管理され、/smart/を含む呼び出しはすべてhttps://webgatewayにリダイレクトされます。 CORSによる頭痛の種を回避するproxy_set_headerには注意が必要です。Web Gatewayが他のサーバーからの呼び出しを拒否しないようにするには、Host ヘッダーの値を変更して、Web Gatewayのアドレスで設定する必要があります。

InterSystems IRIS

次に IRIS を Auth0 で動作するように設定する必要がありますが、そのためには IRIS を OAuth2 クライアントとして設定する必要があります。これを行うには、スーパーユーザ/SYS認証情報で管理ポータルにアクセスし、[System Administration (システム管理)] > [Security (セキュリティ)] > [OAuth 2.0 (OAuth 2.0)] > [Client (クライアント)] オプションにアクセスし、[Create Server Description (サーバ記述の作成)] をクリックし、発行者エンドポイントに Auth0 へのアプリケーション作成時に取得したドメイン値 (https://[MY_DOMAIN]/) を入力します。注意してください!URLは"/"で終わっていなければなりません。最後にSSL/TLSコンフィギュレーションを選択し、Discover and Saveをクリックします。

IRIS client

IRISインスタンスは自動的にAuth0から必要な情報を取得します

Issuer endpoint

先ほど設定したサーバにクライアントを追加するだけです。Client Configuration を押すと、アプリケーションとクライアントの名前を定義する新しい画面にアクセスできます。このクライアント名は、後で FHIR サーバを設定する際に使用します。

FHIRサーバ

プロジェクトの設定を完了する最後のステップは、接続に使用するOAuth2クライアントをFHIRサーバに伝えることです。コンフィギュレーションにアクセスするには、管理ポータルを開き、Health > FHIR > FHIR Configuration > Server Configurationを選択して、画面に表示されているエンドポイントを開きます。最後にOAuth Client Nameフィールドにクライアント・コンフィギュレーションを作成した名前を追加します。

FHIR OAuth Configuration

まとめ

さて、プロジェクトの設定は完了しました。次回は、Angularアプリケーションが各アクターとどのように相互運用するかを見ていきましょう。

ご清聴ありがとうございました!

0
0 74
記事 Toshihiko Minamoto · 6月 12, 2024 4m read

はじめに

先日、@Patrick Jamieson が開催した素晴らしいハンズオンに参加してきました。そこでは、SMART On FHIR が定義したプロトコルに従って Angular アプリケーションを IRIS FHIR サーバーとともに構成したセッションでした。非常に深い興味を得たため、独自に Andular アプリケーションを開発し、そこから学んだことを活用してコミュニティに共有することにしました。

SMART On FHIR

SMART On FHIR について Google ではどのように説明しているのか見てみましょう。

SMART On FHIR は電子健康記録(EHR)システムの情報にアプリケーションがアクセスできるようにするためのデータ規格です。 アプリケーション開発者は、その規格を採用した EHR システムに接続する単一のアプリケーションを作成できます。

SMART On FHIR では、以下の主な概念を処理します。

  • OAuth2 または OpenID による代理認証と承認。
  • 定義されたコンテキストにおける FHIR リソースの操作。
  • HTTPS 通信。

プロジェクトのアーキテクチャ

この演習では、Docker と Auth0 サービスの両方で以下の要素を構成しました。

  • フロントエンドとして機能する Angular で開発されたアプリケーション。このアプリケーションは SMART On FHIR の原則に従って開発されています。
  • Angular で開発されたアプリケーションを公開する NGINX Web サーバーおよびリバースプロキシ。
  • Auth0 が提供する OAuth2 による認証および承認サービス。
  • FHIR サーバーをデプロイし、Docker イメージですでに提供されている Apache サーバーを含む Web ゲートウェイを通じて接続する InterSystems IRIS。

Auth0

ユーザーの認証と承認をこの目的でデプロイされた別の IRIS サーバーに委任することもできますが、ここでは、Auth0 が提供するサービスを使用することにします。

Auth0 とは?

Auth0 は、プラットフォームの認証と承認を処理するメカニズム全体を提供するサービスです。

Auth0 には、あらゆるプロジェクトに簡単に統合できる多様な言語に特化したライブラリもあるため、SMART On FHIR に基づく開発において必ず考慮できるオプションです。

アプリケーションでの Auth0 の使用

OAuth2 の使用は SMART On FHIR を使用するための必要要件であるため、認証、承認、およびアプリケーションアクセスの通常のプロセスに OAuth2 サーバーを含める必要があります。 以下の図は、Auth0 サービスを使ってシステムに送信される情報が辿るパスを示しています。

では、プロセスを分析しましょう。

  • ログインリクエスト:
    1. ログインリクエスト: ユーザーがインターネットブラウザを使用してアプリケーションにアクセスし、ログインをリクエストします。
    2. ログインリクエスト: Angular アプリケーションがそのリクエストを Auth0 サービスに転送します。
    3. ログインページ: Auth0 がユーザーのインターネットブラウザを開発者が作成したページにリダイレクトします。
  • Auth0 での認証:
    1. ユーザー認証情報: ユーザーが、Auth0 に登録されたメールアドレスとパスワードを入力します。
    2. 認証と承認: Auth0 がデータを検証し、ユーザーに割り当てられたコンテキストを含む Access_token を生成します。
    3. Access_token のレスポンスとリダイレクト: Auth0 が生成されたトークンを含むレスポンスをプロジェクト構成に指定された URL にリダイレクトします。
    4. 患者の画面: Angulaar アプリケーションが、個人情報を登録するページをユーザーに表示します。
  • FHIR リソースレコード:
    1. 患者の保存: ユーザーが個人情報をフォームに入力し、Angular アプリケーションがフォームを FHIR Patient リソースのフォーマットで JSON オブジェクトに変換します。
    2. POST リクエスト: Angular アプリケーションが IRIS にデプロイされている FHIR サーバーに、認証トークンとしての access_token をリクエストヘッダーに含めて HTTP POST 呼び出しを送信します。
    3. POST レスポンス: Web ゲートウェイを通じて POST リクエストを受け取ると、IRIS はトークンの有効性とリクエストのコンテキストをチェックします。 すべてが正しければ、受信したリソースを検証して FHIR サーバーに登録し、新しいリソースの作成を示す HTTP 201 を返します。 また、ヘッダーに、新しいリソースに割り当てられた識別子が含められます。
    4. 処理成功: Angular アプリケーションが、主な機能を示す画面にユーザーをリダイレクトします。

ログインが完了すると、プロジェクトに含まれる Auth0 ライブラリが、FHIR サーバーに対して発行するすべてのリクエストを傍受し、Auth0 から受け取るアクセストークンを含めます。

今後の予定

次の記事では、関りのある各システムを構成し、最終的に Angular アプリケーションに接続する方法を説明します。 記事の公開を待ちきれない方は、この記事にリンクされた OpenExchange プロジェクトに関連付する GitHub で README.md をお読みください。Auth0 と InterSystems IRIS の両方を構成する方法が詳しく説明されています。

Awfully Good: Stay Tuned (1992) with John Ritter

0
0 127
記事 Toshihiko Minamoto · 10月 26, 2023 4m read

問題

あわただしい臨床環境では迅速な意思決定が重要であるため、文書保管とシステムへのアクセスが合理化されていなければいくつもの障害を生み出します。 文書の保管ソリューションは存在しますが(FHIR など)、それらの文書内で特定の患者データに有意にアクセスして効果的に検索するのは、重大な課題となる可能性があります。

動機

AI により、文書の検索が非常に強力になりました。 ChromaLangchain のようなオープンソースツールを使用して、ベクトル埋め込みを保存して使用し、生成 AI API 全体でクエリを実行することで、ドキュメント上での質疑応答がかつてないほど簡単になっています。 より献身的に取り組む組織は、既存のドキュメントにインデックスを作成し、エンタープライズ用に微調整されたバージョンの GPT を構築しています。 GPT の現状に関する Andrej Karpathy の講演では、このトピックに関する素晴らしい概要が提供されています。

このプロジェクトは、医療関係者が文書を操作するあらゆるタッチポイントにおいて発生する摩擦を緩和する試みです。 医療関係者が情報を保管し、必要な情報を難なく検索できるように、入力と処理から保管と検索まで、IRIS FHIR と AI を活用しました。

ソリューション

医療関係者が音声メモを記録できるフルスタックのウェブアプリを構築しました。 これらのメモは、Open AI を使って文字起こしされ、要約されてから FHIR サーバーに保管されます。 保管されたドキュメントは、インデックス作成されてから、セマンティック検索で使用できるようになります。  

デモ動画

主な機能

  1. ウェブアプリ - 患者、観察、遭遇に関する診療情報を表示します。 これは Vue.js で構築されています。
  2. 音声データの文字起こし - Open AI Whisper API を使って、録音を正確なテキストに文字起こしします。
  3. テキストの要約 - 文字起こしされた内容を必要なフォーマットで要約してタイトルが付けられます。 症状、診断などの具体的なセクションなどです。 これは、text-da-vinci-003 を使った Open AI テキスト補完 API で行われます。
  4. ドキュメントの保管 - 要約されたドキュメントは、DocumentReference アーティファクトを使って FHIR に保管されます。
  5. セマンティックドキュメント検索 - 保管されたドキュメントはインデックス作成されて、チャンクとして Chroma に保管されます。 これは、Langchain を使用して検索スペースを制限してセマンティック検索に GPT トークンを控えめに使用するために使用されます。 現時点では、使用できるドキュメント数が少ないため、検索時にドキュメントを読み込んでいますが、 非同期的にバックグラウンドでインデックス作成するように変更することが可能です。
  6. ドキュメントのエクスポート - 最後に、ドキュメントを Google Docs に、その他のデータを Google Sheets にエクスポートするオプションがあります。 ユーザーは他の医療関係者や患者とのコラボレーションとやり取りを簡単に行えるように、OAuth を使って特定のアカウントにログインし、ドキュメントをエクスポートすることができます。

試してみましょう

次の GitHub リンクからプロジェクトリポジトリをクローンします: https://github.com/ikram-shah/iris-fhir-transcribe-summarize-export。 提供された指示に従って、プロジェクトをローカルマシン上にセットアップしてください。 期待される動作が得られない場合は、お知らせください。

ご意見とフィードバック

現在使用できる高度言語モデルと大量のデータを合わせることで、ヘルスケア分野の特に文書管理の領域に革命を起こす大きな可能性があります。 以下に、ご意見やフィードバックをお寄せください。 このプロジェクトの背後にある技術的な情報について、さらに多くの記事を投稿する予定です。

このプロジェクトに期待できると思われる場合は、Grand Prix コンテストでこのアプリに投票してください!

0
0 184
記事 Tomohiro Iwamoto · 4月 20, 2023 18m read

Azure ADをOPとして利用する

元のタイトルから外れますがAzure ADをOPとした場合に、Wepアプリケーション(CSP)とSPA+BFF形式のRPにどのような修正が必要かを調べました。
ある程度の差異は想定はしていましたが、思っていたより違うな、という印象を受けました。RP、リソースサーバ側でこれらの差異を吸収する必要がありました。

個人調べです。誤りがあるかもしれませんが、その際はご容赦ください。また、状況は刻々と変わる可能性があります。

相違点

  • frontchannel_logout_session_supportedをサポートしていない

    オプショナルな機能ではありますが、これが、一番残念でした。 sessionを使用したフロントチャネルログアウトをサポートしていないようです。実際、IDトークンに"sid"クレームが含まれていません。

    "http_logout_supported"はtrueなのでSLOは可能ですが、今回用意したクライアントでは実現していません。趣旨からそれますので、Azure使用時のSLOの実現は断念しました。ログアウト操作の対象は常に、ログアウトを実行したアプリケーション単独になります。

    AD FSはサポートしていると思われます。

  • "revocation_endpoint"をサポートしていない

    OpenId Connectディスカバリーに"revocation_endpoint"がありません(つまりサポートしていません)。

    そもそもSLOが無ければ、Revoke(アプリケーション単独でのログアウトに使用)を用意する意味はありませんので、これも断念しました。

  • Userinfoのエンドポイント

    IDトークンに同じ内容が含まれているので、それらを使うよう推奨されています。Azure AD使用時は、ユーザの情報(名前)をIDトークンから取得するよう変更しました。

    ID トークンの情報は、UserInfo エンドポイントで入手できる情報のスーパーセットです。 UserInfo エンドポイントを呼び出すトークンを取得すると、同時に ID トークンを取得できます。このため、UserInfo エンドポイントを呼び出す代わりに、トークンからユーザーの情報を取得することをお勧めします。

    UserInfoエンドポイントへのアクセスも実際に試してみましたが、エラーが発生しました。どうやらこのエンドポイントにアクセスするには、今回のような独自API(リソースサーバ)用ではなく、Graph API用のアクセストークンが要るようです。

    $ export access_token=...SCOPE['openid','profile','offline_access','api://xxxxx/scope1']で発行されたトークン...
    $ curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' https://graph.microsoft.com/oidc/userinfo 
    {"error":{"code":"InvalidAuthenticationToken","message":"Access token validation failure. Invalid audience.","innerError":{"date":"2023-04-20T01:33:53","request-id":"40c464e2-e83f-43e7-bbf5-ec50a9ea3b79","client-request-id":"40c464e2-e83f-43e7-bbf5-ec50a9ea3b79"}}}
    
    $ export access_token=...SCOPE['openid','profile','offline_access']で発行されたトークン...
    $ curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' https://graph.microsoft.com/oidc/userinfo 
    {"sub":"dwHvjtAK6XlYA1VJatjT2GY7dWBBXjhAv8ctUlcUcUE","name":"Alex Wilber","family_name":"Wilber","given_name":"Alex","picture":"https://graph.microsoft.com/v1.0/me/photo/$value","email":"AlexW@xxxxx.onmicrosoft.com"}
    
  • Introspectionをサポートしていない

    Introspectionは未サポートのようです。実行しないように修正しました。

  • アクセストークンのSCOPEパラメータ名が異なる

    SCOPEを示すパラメータ名がscpになっています。

      "scp": "scope1",
    
  • 暗黙のSCOPE

    独自(カスタム)API、今回の例ではscope1、が要求時のSCOPEクレームに含まれる場合、明示的に"openid profile offline_access"を含めても、トークンエンドポイントから取得したアクセストークンのSCOPEにはこれらを含まないようです。クライアントディスクリプション作成時に下記を指定することで対応可能です。

    Set c.AcceptNonStandardImplicitIdToken=1
    

    独自のAPIとMS Graph API用のSCOPEを混在させることは出来ないようです。MS Graph APIが優先されてしまいます。例えば

    SCOPE['openid','profile','offline_access','User.Read']の場合 
    => scp": "openid profile User.Read email","aud": "00000003-0000-0000-c000-000000000000"
    SCOPE['openid','profile','offline_access','api://xxxxx/scope1']の場合 
    => "scp": "scope1","aud": "api://xxxxx",
    SCOPE['openid','profile','offline_access','User.Read','api://xxxxx/scope1']の場合 
    => "scp": "openid profile User.Read email","aud": "00000003-0000-0000-c000-000000000000"
    

    Azure ADはAPI(リソースサーバ)ごとに、アクセストークンを使い分けるという設計思想のようです。アプリケーションが、MS Graph APIと独自APIの両方使いたかったらどうするのか、という話もありますが、今回は独自APIだけ(Userinfoのエンドポイント使用は除外したので)なので、良しとしました。

  • アクセストークン,IDトークンで同一クレーム名に異なる値が設定される

    "iss","aud"値がアクセストークン,IDトークンとで値が異なるため、クライアント側でのチェック対象を変える必要があります。

  アクセストークン
  {
    "aud": "api://xxx-xxx-xxx-xxx-xxx",
    "iss": "https://sts.windows.net/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/",
  }

  IDトークン
  {
    "aud": "xxx-xxx-xxx-xxx-xxx",
    "iss": "https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/v2.0",
  }
  • "aud"の値を複数指定できない

    リソースサーバ側で"aud"をチェックする処理で「リソースサーバのURL」の有無をチェックしている処理が通らなくなります。"aud"にはクライアントアプリケーションのCLIENT_IDがセットされています。ひとまずこの値をチェックするように修正をしました。

  • RefreshToken発行時のリクエストにSCOPE指定が必須

    Client_Secret指定時は、Optionalとなっているscopeですが、指定しないと下記のエラーが出ました。こちらの対応と同様にscopeを追加するために ##class(%ZSYS.OAuth2.Authorization).GetAccessTokenRefresh()を追加しました。

    AADSTS90009: Application 'xxx-xxx-xxx-xxx-xxx'(xxx-xxx-xxx-xxx-xxx) is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier. 
    
  • OIDCのクライアント動的登録機能をサポートしていない

    OIDCのクライアント動的登録機能は無いようです。

環境

Microsoft 365開発者サブスクリプション を有効化して使用しました。

開発者向けの無償のサブスクリプションです。余談ですが、Exchange Online上のメールの受信(pop3+oAuth2認証)テストにもこの環境を使用しました。

主な選択肢は下記のようにしました。

Set up a new Microsoft 365 developer sandboxを選択。
Instantを選択。
Country: AP
Admin: iwamoto
Password: xxxxxx

認証用にSMS用番号を求められるので入力。

これでiwamoto@xyz.onmicrosoft.comのようなアカウント名が付与されます。以後、このアカウントを使用して管理操作を行います。

ほおっておくと、ログインできなくなるような警告が出ました。
「組織のセキュリティを維持するために、セキュリティの既定値群がオンになっています。Microsoft Authenticator アプリをセットアップして、2 段階認証を使用してください。これが>必要になるまであと 14 日です。」
強制的に設定画面が出たので設定を行いました。スマホアプリのMicrosoft Authenticatorを使って表示されるQRコードを読み込むだけです。

Office365の一通りのアプリケーションの管理作業を行えるようになっています。また、ダミーのユーザが作成されていますので、後でログインユーザとして使用します。一番下に自分が登録されています。

付与されたアカウント(私の場合はiwamoto@xyz.onmicrosoft.com)でAzure Portalにログインします。

作業の流れ

先にお断りしておきますと、Azureでの作業は結構面倒です。

以下のような作業の流れになります。

  1. 最新のソースコードの取得

  2. Azure(OP)にアプリケーション(RP)を登録

  3. Azure(OP)にリソースサーバを登録

  4. IRIS(RP)にサーバデスクリプションを登録

  5. IRIS(RP)にサーバデスクリプション/クライアントを登録

  6. IRIS(リソースサーバ)にサーバデスクリプションを登録

  7. IRIS(リソースサーバ)にサーバデスクリプション/クライアントを登録

最新のソースコードの取得

サーバ環境

以前に、git clone実行されている方は、再度git pullをお願いします。始めて実行される方は、サーバ編をご覧ください。

cd iris-oauth2
git pull

クライアント環境

以前に、git clone実行されている方は、再度git pullをお願いします。始めて実行される方は、クライアント編をご覧ください。

cd angular-oauth2-client
git pull

Azure(OP)にアプリケーション(RP)を登録

こちらの内容に沿って作業を進めます。

アプリケーションの登録

アプリケーションの名前: myapp
サポートされているアカウントの種類: この組織ディレクトリのみに含まれるアカウント (MSFT のみ - シングル テナント)
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls

リダイレクト URI, フロントチャネルのログアウト URL追加

pythonコードでテスト実行をしたいので、2個目のリダイレクト先(https://login.microsoftonline.com/common/oauth2/nativeclient)を追加します
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user/MyApp.Logout.cls)を指定します

証明書またはシークレットの追加

新しいクライアント シークレットを追加します。

以下のような情報を取得します。CLIENT_SECRET値はクライアントシークレット作成時にしか見れませんので、このタイミングで必ず書き留めます。

TENANT_ID = 'd8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416' <=[アプリの登録]/[概要]ページの基本で見れる、ディレクトリ (テナント) ID
CLIENT_ID = "f7d8xxx-xxx-xxx-xxx-xxx" <= [アプリの登録]/[概要]ページの基本で見れる、アプリケーション (クライアント) ID
CLIENT_SECRET = "xxxxxxxxxxxxx"  <=クライアントシークレット作成時の「値」のほう。(シークレットIDではない)

これでIssuer エンドポイント(https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/v2.0)が確定します。後で、IRISへの登録時に使用します。

SCOPE追加

「APIの公開」画面で「Scopeの追加」を押してscope1を追加します。既定ではapi://xxxxxというプリフィックスが付きます。

Python + o365 でテスト

Azure AD側の設定が正しく行えているかの事前確認として、Pythonのo365パッケージを使用してトークン取得を行います。

こちらの記事を参考にさせていただきました。

get_token.pyの下記を取得した値に変更して実行します。

TENANT_ID = 'd8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416'
CLIENT_ID = "f7d8xxx-xxx-xxx-xxx-xxx"
CLIENT_SECRET = "xxxxxxxxxxxxx" 

SCOPES = ['openid','profile','offline_access','api://f7d8xxx-xxx-xxx-xxx-xxx/scope1']

実行するとURLが表示されるので、ブラウザにペーストします。(初回実行時は)ログイン実行を促されますので、さきほど取得したアカウント(私の場合、iwamoto@xyz.onmicrosoft.com)でログインします。リダイレクトされて空白ページに移動しますので、そのURLをpythonのプロンプトにペーストして処理を終了します。

C:\git\iris-o365>python get_token.py
Visit the following url to give consent:
https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/oauth2/v2.0/authorize?response_type=code&client_id=f7d8xxx-xxx-xxx-xxx-xxx&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient&scope=profile+openid+api%3A%2F%2Ff7d8xxx-xxx-xxx-xxx-xxx%2Fscope1&state=K1p7qcbW0PWM29nWpdZRwoMyaWPojA&access_type=offline
Paste the authenticated url here:
https://login.microsoftonline.com/common/oauth2/nativeclient?code=0.AUoAek702LUSWUq0...  [ペースト]
[エンターキーを押下]
Authentication Flow Completed. Oauth Access Token Stored. You can now use the API.
Authenticated!
C:\git\iris-o365>

token.jsonというファイルが出来ますので、access_token, id_tokenをjwt.io等でデコードして内容を確認します。出来ていなければ、何かがおかしいので、設定を見直してください。これが成功しないと、以降の操作も成功しません。

jwt.ioによるとscp値は下記でした。

 "scp": "scope1",

IDTokenの"iss","aud"値がATのそれらと値が異なる事がわかります。これはRP内でATとIDトークンの両方をチェックしようとすると良からぬ影響が出ます。今回はRPではIDトークンのバリデーションだけを行うことで対応しています。

全てのアプリケーションを登録

同様にmyapp2,bff,bff2,という名前でアプリケーションを登録し、それぞれのCLIENT_ID, CLIENT_SECRET, SCOPEを記録しておきます。

面倒です。動的登録が出来ればな...と思いました。WebアプリケーションとSPA+BFF用に、各々2個のクライアント(実行されるIRISネームスペースが異なる)を登録するため、計4通り存在します。ひとまずmyapp(既に登録済み)とbffだけでも良いです。

取得したクライアントID,クライアントシークレット,SCOPE値をIRIS実行環境に反映する必要があります。 本例では、それらをJSONファイル化しておき、実行時にロードするという方法を採用しました。

テンプレートをコピーして使用します。先ほどのmyappの内容を含め、伏字の値を実際の値で置き換えてください。

cd config
cp azure.json.template azure.json
vi azure.json

Azure ADへの登録内容とazure.jsonへの反映箇所の関係

アプリケーションの名前: myapp (今までの作業で登録済みです)
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user/MyApp.Logout.cls)
azure.jsonでの名称: USER_CLIENT_APP

アプリケーションの名前: myapp2
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user2/MyApp.Logout.cls)
azure.jsonでの名称: USER2_CLIENT_APP

アプリケーションの名前: bff
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/myapp/#/logout-bff)
azure.jsonでの名称: BFF_BFF_APP

アプリケーションの名前: bff2
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/myapp2/#/logout-bff)
azure.jsonでの名称: BFF2_BFF_APP

Azure(OP)にリソースサーバを登録

同様にリソースサーバを登録します。リダイレクトは要りません。

アプリケーションの名前: myrsc
サポートされているアカウントの種類: この組織ディレクトリのみに含まれるアカウント (MSFT のみ - シングル テナント)

同様に新しいクライアント シークレットを追加します。

アプリケーションと同様に、ClientId, ClientSecretの値をazure.jsonの"RESSERVER_APP"下に反映しておきます。

azure.jsonの完成形

伏字だらけで少し分かりにくいですが、全てを埋めたazure.jsonは下記のようになります。

{
	"OP": "azure",
	"tenantID":"d8f44e7a-xxx-xxx-xxx-xxx",
	"issuerEndpoint":"https://login.microsoftonline.com/d8f44e7a-xxx-xxx-xxx-xxx/v2.0",
	"apps":{
		"USER_CLIENT_APP":{
			"ClientId":"e20dd7f3-xxx-xxx-xxx-xxx",
			"ClientSecret":"3bU8Q~9g8xLaAi81WshoTLZuh3rWwDO7NUaDKaa_",
			"SCOPES":"api://e20dd7f3-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/{{{HOSTNAME}}}/csp/user/MyApp.Logout.cls"
		},
		"USER2_CLIENT_APP":{
			"ClientId":"53bb346c-xxx-xxx-xxx-xxx",
			"ClientSecret":"oNe8Q~J-5iPyAj_zHd8r3axXxl9ffJRWrVZ0Sa~N",
			"SCOPES":"api://53bb346c-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/{{{HOSTNAME}}}/csp/user2/MyApp.Logout.cls"
		},
		"BFF_BFF_APP":{
			"ClientId":"a4ef08b0-xxx-xxx-xxx-xxx",
			"ClientSecret":"D2A8Q~CuxCGHYeXmUAqD7wjtY-gucdQU44Yj4b-U",
			"SCOPES":"api://a4ef08b0-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/myapp/#/logout-bff"
		},
		"BFF2_BFF_APP":{
			"ClientId":"dc04f6cd-xxx-xxx-xxx-xxx",
			"ClientSecret":"5Br8Q~h~CzkJW1z2NSWii0uAq0HuPvoW46cvhaKj",
			"SCOPES":"api://dc04f6cd-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/myapp2/#/logout-bff"
		}
	},
	"rsc":{
		"RESSERVER_APP": {
			"ClientId":"9842ba63-xxx-xxx-xxx-xxx",
			"ClientSecret":"7vJ8Q~PS7wFw_15SY.V3whxU2p3STBuvAUkTydjH"
		}
	}
}

このファイルを用意することが、下記の操作を行ったことになります。

  • IRIS(RP)にサーバデスクリプションを登録
  • IRIS(RP)にサーバデスクリプション/クライアントを登録
  • IRIS(リソースサーバ)にサーバデスクリプションを登録
  • IRIS(リソースサーバ)にサーバデスクリプション/クライアントを登録

ビルド

サーバ環境

cd iris-oauth2
cp webgateway* iris-webgateway-example/
./build.sh

クライアント

ビルドには、稼働中のサーバ環境が必要なので、この時点で行うことはありません。

実行

サーバ環境

config/azure.jsonを修正済みであることを確認した上で下記を実行してください。

./up-azure.sh
    ・
    ・
Useful links...
Web Gateway | http://webgw.localdomain/csp/bin/Systems/Module.cxw
RSC #1 SMP | http://webgw.localdomain/irisrsc/csp/sys/%25CSP.Portal.Home.zen
RSC #2 SMP | http://webgw.localdomain/irisrsc2/csp/sys/%25CSP.Portal.Home.zen
CSP based client server3 SMP | http://webgw.localdomain/irisclient3/csp/sys/%25CSP.Portal.Home.zen
CSP based client App3-1 | https://webgw.localdomain/irisclient3/csp/user/MyApp.Login.cls
CSP based client App3-2 | https://webgw.localdomain/irisclient3/csp/user2/MyApp.Login.cls
Angular based clien App | https://webgw.localdomain/myapp/ https://webgw.localdomain/myapp2/

クライアント

サーバ環境が起動した事を確認の上、実行します。

cd angular-oauth2-client
./build_and_deploy.sh
あるいは
./ng-start.sh (デバッグ実行)

操作方法

クライアント編と同じです。WebアプリケーションSPA+BFFを実行できます。

前回と異なり、ログインを実行すると、Azure ADのログイン画面が表示されますので、Microsoft 365開発者サブスクリプションで作成されたユーザでログイン(adelev@xxxxx.onmicrosoft.com等)します。

エラー

下記エラーが出た場合、IRISサーバ環境が古いままです。

$ ./build_and_deploy.sh
&#x2714; Browser application bundle generation complete.

Error: src/app/display-info-bff/display-info-bff.component.ts:44:26 - error TS2339: Property 'OP' does not exist on type '{ clientId: string; authUri: string; logoutUri: string; tokenUri: string; userinfoUri: string; redirectUri: string; scope: string; frontchannel_logout_uri: string; post_logout_redirect_uri: string; }'.

44     if (environment.auth.OP==='iris') {
                            ~~

(番外編)SAML認証

同じAD環境を使用して、コミュニティ記事(https://community.intersystems.com/post/work-saml-iris)のSAML認証をAzureで試してみました

ngrok は必要ありません。

IRISでのSAMLのサポートはoAuth2に対するそれほどは手厚くありません。下記コマンドでSAML応答(XML)の署名を確認して、その正当性を確認しています。

Set tSC = ##class(Ens.Util.XML.SecuritySignature).ValidateSAML(tSAML, tValSpec, X509File.Filename, tClockSkew)

実行方法

実行用のCSPアプリケーションを表示。

AD>エンタープライズ アプリケーション>すべてのアプリケーション>+新しいアプリケーション>独自のアプリケーションの作成を選択。

お使いのアプリの名前は何ですか?: myapp-saml ギャラリーに見つからないその他のアプリケーションを統合します (ギャラリー以外) 「作成」を押下。

シングルサインオンの設定「作業の開始」>シングル サインオン方式の選択で「SAML」を選択。

下記の必須項目に、アプリに表示されている内容をそのまま使用する。

基本的な SAML 構成
識別子: https://intersystems.com/saml/E106172E-DB35-11ED-B731-0242C0A82802
応答URL: https://webgw.localdomain/irisclient3/csp/user/SAML.MyApp.cls

[保存]を押下。

識別子はユニークであれば何でも良い。上記はアプリ内で生成した固定文字列"https://intersystems.com/saml/"+GUID。

「属性とクレーム」で「編集」を押し、詳細設定 => SAML クレームの詳細オプション 編集 属性名の形式を含める:有効 <=有効にする [保存]

この作業は不要(今回のケースでは無意味)かもしれない。

「SAML 証明書」から下記を全部ダウンロードする。
証明書 (Base64): myapp-saml.cer
証明書 (未加工): myapp-saml(1).cer
フェデレーション メタデータ XML: myapp-saml.xml

「ユーザとグループ」で「ユーザまたはグループの追加」を押し、ログインするユーザ(誰でも良いです。AlexW@xxxx.onmicrosoft.com)を追加。「割り当て」を押下して追加を完了する。

アプリで「ファイルを選択」を押し、先ほどダウンロードしたメタデータファイル(フェデレーション メタデータXML)を選択し「適用」を押す。 成功するとIdentity providerの情報やSAML要求の内容が表示される。画面最下の「Login」押下すると、Azureの「アカウントを選択する」画面が表示される。 先ほど追加したユーザ(AlexW@xxxx.onmicrosoft.com)で、ログイン。

ログインすると、画面にSAMLResponseの内容が表示される。

Validation: Success
NameID: AlexW@xxxx.onmicrosoft.com

と表示されていればログイン成功です。

オリジナル記事にもあるように、このアプリケーションは、ログインユーザの情報を取得(および署名のチェック)するだけで、アプリケーションのユーザとしての認証の仕組みは備えていません。必要に応じて、上記で得たNameIDを使用して、なんらかの方法でIRISのユーザとして認証してください。

オリジナル(GCP対応)からの修正点

オリジナル(GCP対応)を修正しています。 Azure発行のフェデレーション メタデータ XMLにnameIDFormat属性が含まれていなかったため、決め打ちで設定しています。

フォーマットについて、ドキュメント には、下記のように書いてあるが、emailAddressが返却されたのでemailAddressを採用。

現在 Azure AD では、次の SAML 2.0 のNameID フォーマット URI をサポートしています: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent。

;Azure AD includes no NameIDFormat attribute. So nameIDFormat becomes null.
If $D(nameIDFormat)=0 {
  Set @..#SettingsGN@("nameIDFormat") = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  #;Set @..#SettingsGN@("nameIDFormat") = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
  
}
Else {
  Set @..#SettingsGN@("nameIDFormat") = nameIDFormat
}
0
0 646
記事 Tomohiro Iwamoto · 4月 7, 2023 21m read

クライアントアプリケーション編

IRISだけでoAuth2/OpenID ConnectのSSO/SLO環境を実現する/サーバ編」 のクライアントアプリケーション編です。サーバ編の構成が既に稼働していることを前提にしています。

既にサーバ編でgit clone実行された方(ありがとうございます)、若干の変更がありますのでgit pullと./build.shの実行をお願いします。

oAuth2クライアントアプリケーション(OICD用語ではRP。以下RPと称します)の形態として、5種類用意しています。


形態ライブラリ登録クライアントタイプSSOSLO
WebアプリケーションIRIS/CSPConfidential実装あり実装あり
SPAAngularPublic実装あり実装なし
SPA+BFFAngular,IRIS/RESTConfidential実装あり実装あり
PythonoauthlibConfidentialN/AN/A
curlN/AConfidentialN/AN/A

PythonやcurlがConfidential(client_secretの秘匿性を十分に保てる)か、というと微妙ですが、あくまで利用者は開発者のみ、という位置づけです。

Webアプリケーション(非SPA)

従来型のWebアプリケーション、Confidential Clientの例です。

                      login/logout
      +-----------------------------------------------------+
      |                                                     | 
ブラウザ ---Apache--> CSPベースのWebアプリケーション +--> 認可サーバ
(ユーザセッション,   (CSP SessionID,AT等)            |    (ユーザセッション)
CSP sessionID)                                       +--> リソースサーバ

IRISに限りませんが、SSOで得たユーザ情報(subクレーム)を、RPやリソースサーバで使用するユーザにどのようにマッピングするか設計・実装する必要があります。ここではRPの実装例として、下記の2種類を用意しています。

  • IRISのユーザ認証をしない

    IRISユーザとの認証(マッピング)は一切行わず、RPのアプリケーションコード内で取得したoAuth2/OIDC関連の情報のみでアクセス制御する形式です。
    サーバ編で使用したWebアプリケーション#1a,1bがこれに該当します。

    内容としては、この記事でGoogleをOPとした例を元に、IRISをOPに変更したものです。
    RPの一部として、認可コードフローの開始を行っています。

    この例ではRP上はIRISユーザ、UnknowUser(RoleはWebアプリケーションで付与)で動作します。

  • ログインフォームをカスタマイズして、IRISユーザとマッピングする

    IRISのログインページのカスタマイズ機能と代行ログインの仕組みを利用して、oAuth2/OIDC関連の情報取得とRPで使用するIRISユーザとの認証(マッピング)を行います。
    RP実行ユーザはSSOログインに使用したユーザ名に"OAuth"を付与した名称を持つ代行ユーザになります。

    例えば、testというユーザでSSOした場合、RP上のIRISユーザ名はAuth2test(RoleはZAUTHENTICATEで付与)になります

    Webアプリケーション#2がこれに該当します。

    RPは、認証が完了した状態からの開始になります。上記のLogin.clsが担っていた役割は、カスタムログインページZAUTHENITICATEに隠ぺいされます。

    現時点で、こちらの方法はあまり深堀りしていません。もう少し製品サイドの機能拡充を待ちたいと思います...。

SPA

Angularで動作する、Public Clientの例です。

                      login/logout
      +-----------------------------------------+
      |                                         | 
ブラウザ(SPA) -----Apache--+--> Webpack         |
(ユーザセッション)         +-------------> 認可サーバ
                           |               (ユーザセッション)           
                           +-------------> リソースサーバ

ng(AngularのCLI)でビルドを行い、ビルド成果物(webpack)をapacheに配布します。 SPAが直接アクセストークンを入手、使用しますので、セキュアな環境が必要な場合、その漏洩対策が必要とされています。

その対策が大変なので、BFFという仕組みが提案されています

現状、SLOは機能しません。正確には、認可サーバからのログアウトは実行されます(その結果、同じユーザセッションに属するトークンは破棄されます)が、SPAベースの各RPに対して、SLOされたというイベントを安全に伝える方法が無いため、各ブラウザタブのsessionStorageに保存されているアクセストークンの破棄などを行うRP側のlogout処理をトリガ出来ないためです。

トークンがExpire(デフォルトで3,600秒)すればRP自身で気づくきっかけになります

RPが定期的にuserinfo等を取得して、ユーザセッションが有効か確認するという方法はあり得ますが、認可サーバへのストレスが大きいです。

SLO実行後、認可サーバ上のユーザセッションは終了していますので、その後のRPでのトークンの更新やログアウト,リソースサーバでのGetIntrospectionは失敗します。

OpenID Connect Session Management 1.0のSession Status Change Notificationがこの仕組みに関する仕様です。
RPの画面にRP用、OP用のiframeをhiddenで用意しておいて、その両者(クロスドメインの可能性あり)でデータ交換することで、ログアウトされた(セッションのステータスが変更した)ことを知る...みたいな内容です。 IRISはOpenID Connect Session Management 1.0をサポートしていません。

IRISが対応しているのはfront channel logoutです。

もし深堀りされたいなら、こちらなどが参考になりそうです。

SPA+BFF

AngularにBFF(Backend For Frontend)を追加することで、Confidential Client化する例です。

                      login/logout
      +-----------------------------------------------------+
      |                                                     | 
ブラウザ(SPA) -----Apache--+--> Webpack                     |
(ユーザセッション,         +--> REST ------------+----> 認可サーバ
sessionID)                      (sessionID,AT等) |      (ユーザセッション)
                                                 +--> リソースサーバ

BFFについては、こちらの説明がわかりやすかったです。

ざっくりと言ってしまえば、SPA単独と比べてよりセキュアで、RPの種類が増えた際にアーキテクチャ上のメリットがある、ということです。

本BFF実装は、下記のIRISのRESTエンドポイント(そこで使用されるRP用のAPI)をAngularから呼び出す事で実現しています。

Angular側の大半の処理はbff.service.tsにあります。IRISのRESTエンドポイントはBFF.REST.clsで実装されています。


AngularIRIS実行されるAPI
接続、ログイン/getauthurl##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint()
userinfo取得/userinfo##class(%SYS.OAuth2.AccessToken).GetUserinfo()
リソースサーバアクセス/call通常のhttpリクエスト+アクセストークン
トークン更新/refresh##class(%ZSYS.OAuth2.Authorization).GetAccessTokenRefreshById() *1
ログアウト/getauthurl##class(%ZSYS.OAuth2.Authorization).GetLogoutEndpoint() *2
トークン破棄/revocation##class(%SYS.OAuth2.AccessToken).RevokeToken()

(*1)トークン更新は独自にメソッドを実装しています。
(*2)現在、独自メソッドを実装していますが、V2023.2以降で標準APIに反映される予定です。

ユーザエージェント-BFF間のセッション維持のために、##class(BFF.REST).GetAuthorizationCodeEndpoint()で、サーバ側で独自にsessionID(httponlyクッキー)発行しています。セッションIDの生成ロジックはセキュリティ強度に直結しますので、適宜見直してください。

Set sessionid=##class(%OAuth2.Utils).Base64UrlEncode($system.Encryption.GenCryptRand(12))  // 要変更
  ・
  ・
Do %response.SetCookie(..#SESSIONCOOKIE,sessionid,,..#COOKIEPATH,,..#SECURECOOKIE,1) ; secure,httponly 

Python

これはアプリケーションとは呼べないです。
認可コードフローを利用する、デバッグ、実験用途のpythonクライアントです。Confidential Clientとして登録しています。 ブラウザで実行すると、リダイレクトの連鎖の中で見落としてしまう内容を捕捉するために使用しました。

python +--> ブラウザ -----Apache--------> 認可サーバ
       |    (ユーザセッション)     (ユーザセッション)
       +---Apache---> リソースサーバ

curl

これはアプリケーションとは呼べないです。
これも、サーバの動作確認のためのデバッグ用途です。Confidential Clientとしています。 リソースオーナー・パスワード・クレデンシャルズのクライアントとして使用します。これもデバッグ、実験用途なので、受信したトークンの有効性チェック等は一切行っていません。

リソースオーナー・パスワード・クレデンシャルズの使用は禁止されるようです。

curl --Apache--+--> 認可サーバ
               +--> リソースサーバ

クライアント認証メソッドの設定

認可サーバへのRP登録時に、RPがトークンエンドポイントにアクセスする際のクライアント認証メソッドを指定します。

SPAだけは「エンコードされたボディから(client_secret_post)」を選択しています。残りは全て「ベーシック(client_secret_basic)」になっています。

下記は、MyApps.Installer.clsからの抜粋。

Set c.Metadata."token_endpoint_auth_method" = "client_secret_basic"

「エンコードされたボディから」はPOSTリクエストのBodyにパラメータとして渡す形式、「ベーシック」は、HTTPヘッダでAuthorization: Basic BASE64(client_id:client_secret)を指定する形式です。

IRISに用意されているRP用のAPI群

IRISベースのRP(CSPベースのWebアプリケーション、SPA+BFFのBFFサーバ)の場合、RP用に用意された以下のAPI群を使用できます。

詳細は、ドキュメントを参照ください。

ログイン時

##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint()は、認可サーバへのログインを実行するためのURLを返却します。ユーザがこのURLをクリックすることで、認可コードフローが開始します。

使用例はMyApp.Login.clsを参照ください。

set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
  $$$APP,
  scope,
  "https://webgw.localdomain/irisclient/csp/"_ns_"/MyApp.AppMain.cls",
  .properties,
  .isAuthorized,
  .sc)

$$$APPはRPを認可サーバに登録した際に使用した登録名(USER_CLIENT_APP等)です。scopeには認可を要求するスコープ("openid profile scope1"等)を指定します。

返却されるURLは次のような内容になります。いわゆる承認リクエストです。

https://webgw.localdomain/irisauth/authserver/oauth2/authorize
  ?response_type=code
  &client_id=01yJ2zW0K9XQqrroIB4b7PLci3NkJBegm0b_kBcFhgw
  &redirect_uri=https%3A//webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.Response.cls
  &scope=openid%20profile%20scope1
  &state=gWr5ipHAtskDhf0fFbNtxxJWnwY
  &nonce=paEDbHweG2xiJxWxotTfaJPbdB4
  &code_challenge=8X5l4fxCNOdLj-u87Mu3UwRH96dxYpjoGcv6Z-JZmEc
  &code_challenge_method=S256

RPのリダイレクト先を指定するredirectクエリパラメータ値がOAuth2.Response.clsになっていますが、これはIRISベースのRP専用のランディングページになっていて、RPがするべきこと(認可コードの取得、stateのチェック、アクセストークンリクエストの発行、などなど)を行ってくれます。

RPの登録内容(クライアントのリダイレクトURL)も、下記のようにホスト名、ポート番号、プレフィックスのみを指定すれば、このリダイレクト先が自動選択されます。

CSP(非REST)ベースのアプリケーションは、CSPセッションという独自のセッションID(%session.SessionId)と保存領域を作成します。oAuth2クライアントとして一度認証・認可を受けると、認証関連の情報(アクセストークン,有効期限など)を、このCSPセッションIDを主キーとしたテーブルに保存します(さきほどのOAuth2.Response.clsが実行)。

$ docker compose exec irisclient iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"
[SQL]%SYS>>select ApplicationName,SessionId,Expires,ResponseType,Scope,CONVERT(VARCHAR(10),AccessToken) from OAuth2.AccessToken

ApplicationName SessionId       Expires ResponseType    Scope   Expression_6
CLIENT_APP      S1YXEXP9SP      1677551976      code    openid profile scope1 scope2 scope99    eyJ0eXAiOi

一方、ブラウザにはCSPセッションID(を拡張したデータ)のみがセッションクッキー(HttpOnly, デフォルト設定ではSameSite=Strict)で保存されます。

この辺りは従来のCSPのメカニズムそのものです。例えば CSPSESSIONID-SP-443-UP-irisclient-csp-user- というクッキー名になります。

認可コードフローの終了時には、GetAuthorizationCodeEndpoint()の第3引数で指定したURL、この場合はMyApp.AppMain.clsにリダイレクトされます。

認証済みか否かの確認

##class(%SYS.OAuth2.AccessToken).IsAuthorized()はRPが認証済みかどうかを確認するAPIです。

認可コードフローのリダイレクト先であるOAuth2.Response.clsはRPが認証済みかどうかを確認するための情報元をRPに格納します

2つの事を行ってくれます。

  • その情報の状態によってRPが既に認証済みかどうかの確認を行います

  • 認証済みである場合、アクセストークン、IDトークンなどを返却します

MyApp.Login.clsのOnPreHTTP()や、MyApp.AppMain.clsで使用されています。

RPが既に認証済みの場合は、MyApp.Login.clsのOnPreHTTP()が実行され、IsAuthorized()がTrueとなり、認可サーバを経由することなく、MyApp.AppMain.clsにリダイレクトされます。

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
  #dim %response as %CSP.Response
  set scope="openid profile "_..#SCOPES
  if ##class(%SYS.OAuth2.AccessToken).IsAuthorized($$$APP,,scope,.accessToken,.idtoken,.responseProperties,.error) {
    set %response.Redirect="MyApp.AppMain.cls"
  }
  Return $$$OK
}

IDトークンの有効性の確認(Validation)

##class(%SYS.OAuth2.Validation).ValidateIDToken()はIDトークンの有効性を確認します。

どのような確認を行うかは、OIDC で定められています。

MyApp.AppMain.clsで使用されています。

set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
    $$$APP,
    idtoken,
    accessToken,
    ..#SCOPES,
    ..#RSCURL1,  // RSCURL2の存在チェックはしていないので注意
    .jsonObject,
    .securityParameters,
    .sc)

トークンの受信後には署名の有無の確認を行います。

        Set isSigned=$Data(securityParameters("sigalg"))#2
        if 'isSigned="" { 
            write "署名されていません !<br>",!
        }

Introspection

##class(%SYS.OAuth2.AccessToken).GetIntrospection()は認可サーバにアクセストークンの有効性を問い合わせます。

認可サーバへのトラフィックが発生するため、opaqueのアクセストークンの場合は必須ですが、JWTの場合は任意とされています

MyApp.AppMain.clsで使用されています。

set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection($$$APP,accessToken,.jsonObject)

ログアウト時

##class(%SYS.OAuth2.Authorization).GetLogoutEndpoint()は、SLO(シングルログアウト)を実行するためのURLを返却します。ユーザがこのURLをクリックすることで、RP Initiatedのフロントチャネルログアウトが開始します。

Set ns=$ZCVT($NAMESPACE,"L") Set fclouri="https://webgw.localdomain/irisclient/csp/"_ns_"/MyApp.Login.cls"
Set url=##class(%SYS.OAuth2.Authorization).GetLogoutEndpoint($$$APP,fclouri)

返却されるURLは次のような内容になります。

https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls
  ?register=irzgE0VENqa51QpmvGcmG-nDWn0

このURLをGETすると、さらに認可サーバのlogoutエンドポイントへのURLにリダイレクトされます。RP-Initiated Logoutのためのエンドポイントです。

 https://webgw.localdomain/irisauth/authserver/oauth2/logout
  ?id_token_hint=eyJ0eXAiO.....
  &post_logout_redirect_uri=https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls
  &state=Pc73yDQFouYO1Y-a0PioOI3qRtw

RPのリダイレクト先を指定するpost_logout_redirect_uriクエリパラメータ値がOAuth2.PostLogoutRedirect.clsになっていますが、これはIRISベースのRP専用のランディングページになっていて、RPがするべきこと(RPに保存されているトークンの除去等)を行ってくれます。

ログアウト終了時には、GetLogoutEndpoint()の第2引数で指定したURL(この場合は、ログインページであるMyApp.Login.cls)にリダイレクトされます。

トークンの失効処理(Revocation)

##class(%SYS.OAuth2.AccessToken).RevokeToken()は指定したトークンを失効させます。

ユーザセッション使用時に、単独のアクセストークンのみを失効させることで、特定のアプリケーション単独のログアウトを実現します。ログイン時はSSOのメリットを受けつつ、ログアウトは個別に行いたい場合に使用できます。

CSPベースのWebアプリケーションの場合、ログアウト時にはCSPセッションを終了させますが、本メソッドの呼び出しは、CSPセッションの終了時に自動実行されるため、アプリケーションコードで明示的に呼び出す必要はありません。RESTを使用する場合、(USESESSIONパラメータを1にしない限り)CSPセッションは使用しませんので、明示的に呼び出す必要があります。

本例では、MyApp.AppMain.clsのOnPreHTTP()で下記を実行しています。

Do %session.Logout() 
Set %session.EndSession=1 ; will call OAuth2.SessionEvents.OnEndSession()
Set %response.Redirect="MyApp.Login.cls"

リソースサーバへのRESTアクセス

リソースサーバのRESTエンドポイント呼び出し時は、##class(%SYS.OAuth2.AccessToken).AddAccessToken()を使用してアクセストークンを追加します。追加方法は第2引数で指定します。省略時はHTTPヘッダーに"Authorization: Bearer"を追加します。

set httpRequest=##class(%Net.HttpRequest).%New()
// AddAccessToken adds the current access token to the request.
set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
    httpRequest,,
    ..#SSLCONFIG,
    $$$APP)
if $$$ISOK(sc) {
    Set url=..#RSCURL2_"/private" w "URL:"_url_"</BR>",!
    set sc=httpRequest.Get(url)
}

導入手順

複数のgitをcloneしますが、下記のフォルダ構造にしておけば、各種手順やシェルを修正することなく(最もストレスなく...)動作させる事が出来ます。

Linux
(任意)/iris-oauth             <= サーバ編で導入済みのはず
(任意)/angular-oauth2-client  <= git clone https://github.com/IRISMeister/angular-oauth2-client.git

Windows
C:\git\python-oauth2-client   <= git clone https://github.com/IRISMeister/python-oauth2-client.git
C:\git\angular-oauth2-client  <= ngビルドをWindowsで動かすのであれば

以下、(任意)は$HOME/gitとしています。

Webアプリケーション

導入手順

不要です。サーバに同梱されています。

起動方法

こちらはIRISサーバで完結するので、単独の起動方法はありません。サーバ用のコンテナ起動時に自動起動します。

アクセス方法



操作については、サーバ編を参照ください。

SPA(Angular)

F5(リロード)対策としてアクセストークンをsessionStorageに保存しています。XSS脆弱性に対してご注意ください。

導入手順

LinuxあるいはWindowsで実行します。
ビルド・実行にはnode, npm, ngが必要です。私の環境は以下の通りです。Windowsでも同じことが出来ます。

Windowsの場合、./build_and_deploy.ps1, ng-start.ps1内の$env:gitrootの値を環境にあわせて修正してください。

$ ng version

Angular CLI: 15.1.6
Node: 16.16.0
Package Manager: npm 9.3.0
OS: linux x64

Angular: 15.1.5
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1501.6
@angular-devkit/build-angular   15.1.6
@angular-devkit/core            15.1.6
@angular-devkit/schematics      15.1.6
@angular/cli                    15.1.6
@schematics/angular             15.1.6
rxjs                            7.8.0
typescript                      4.9.5

下記の手順でビルドしたangularアプリケーションのディストリビューション(./dist/*)を、iris-oauth2のhtdocs/下にコピーすることで使用可能になります。

  1. Git Clone

    カレントディレクトリが./iris-oauth2であることを確認してください。その1階層上のディレクトリに移動して、cloneします。

    $ pwd
    /home/irismeister/git/iris-oauth2
    $ cd ..
    $ git clone https://github.com/IRISMeister/angular-oauth2-client.git
    $ cd angular-oauth2-client
    
  2. $ npm install
    $ ./build_and_deploy.sh   (Windowsの場合は>powershell ./build_and_deploy.ps1)
    

    このシェルは下記を行います。

    • client_id, client_secretをサーバ配下のファイル(environment*.ts)から取得
    • NGビルド実行。 ターゲットは/myapp/
    • htdocs/myapp/以下にビルド成果物をコピー
    • NGビルド実行。 ターゲットは/myapp2/
    • htdocs/myapp2/以下にビルド成果物をコピー

    下記のように表示されれば成功です。

    $ ./build_and_deploy.sh
    &#x2714; Browser application bundle generation complete.
    &#x2714; Copying assets complete.
    &#x2714; Index html generation complete.
          ・
          ・
          ・
    Build at: 2023-04-05T07:23:58.705Z - Hash: 67aabdbbe8ad0bfa - Time: 6796ms
    deploying to iris-oauth2
    done
    Go to https://webgw.localdomain/myapp/ or https://webgw.localdomain/myapp2/
    $
    

以後、iris-oauth2をリビルドするなどで、client_id,client_secretが変更された場合は、build_and_deploy.shを再実行してください。

起動方法

こちらはApacheを使用しますので単独の起動方法はありません。サーバ用のコンテナ起動時に自動起動します。

アクセス方法


名称エンドポイント
SPAアプリケーション/myapp
SPAアプリケーション/myapp2

両者は、client_idが異なる(つまり別のRPとみなす)だけ内容は同じです。下記のような「最高にクール」な画面が表示されます。

「BFF接続テスト」を押すとBFFとの疎通確認を行い、成功の場合IRISバージョンを画面表示します。

「SPAでログイン」を押すと、SPAのみで認証を行います。下記のような画面が表示されれば成功です。適当にボタンを押して(処理内容はボタンが示す通りです)動作確認してください。

修正・デバッグ

修正・デバッグ目的で、Angular Live Development Serverで起動することができます。

./ng-start.sh (Windowsの場合は>powershell ./ng-start.ps1)

Compiled successfullyという起動メッセージが表示されたら、Angularアプリケーション(live)にアクセスします。

よくあるエラー

Unexpected request - client_idと表示される場合、(恐らく)client_idが正しく反映されていません。build_and_deploy.shで直ります。

今後の修正

今後のドキュメントへの加筆・修正等はこちらで行います。

SPA(Angular)+REST+BFF

Webアプリケーションではセッション管理にCSPセッションを使用していますが、SPA+BFFでは独自のセッションを作成しています。

sessionidというクッキー名です。

SLOが機能します。

導入手順

SPA(Angular)に同梱されています。

起動方法

SPA(Angular)と同時に起動されます。

アクセス方法

SPA(Angular)と同じです。

「SPA+BFFでログイン」を押すと、SPA+BFFで認証を行います。下記のような画面が表示されれば成功です。SPAとほぼ同じですが、こちらにはBFFサーバの情報表示とトークンの取り消し機能が追加されています。

Python

導入手順

ブラウザをポップアップさせる必要があるため、Windowsで稼働させます。
DOS窓から下記を実行します。

C:\git> git clone https://github.com/IRISMeister/python-oauth2-client.git
C:\git> cd python-oauth2-client

Linux上のssl/web/all.crt(Webサーバの証明書チェーン)をローカルのc:\tempにコピーしてください。

$ pwd
/home/irismeister/git/iris-oauth2
$ cp ssl/web/all.crt /mnt/c/temp

クライアント情報を起動時に出力された内容(client/credentials_python.json)で上書きコピー(無ければ新規作成)します。
コピー先は、cloneを実行したパスに合わせてください。

$ cp client/credentials_python.json /mnt/c/git/python-oauth2-client/credentials.json

アクセス方法

pythonコードを起動します。

C:\git\python-oauth2-client>run.bat

ブラウザ上に、認証画面が表示されますので、「引き受ける」を押します。Please close the window.と表示されたら、ブラウザは以後不要なので閉じます。 DOS窓上にサーバから得た情報が出力されているはずです。

C:\git\python-oauth2-client>python request.py

***** auth_url *****
https://webgw.localdomain/irisauth/authserver/oauth2/authorize?response_type=code&client_id=EKj6Bww0yiFgeuYUxtkyzfge84-keMy20uE5xTmlWPk&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&scope=openid+profile+scope1+scope2&state=aiq21soYtKWlzomgChYUlzVgjpby1v&code_challenge=AIu6-HIPnl1MKWIxImJ3Earg0ezfcUbNF75hLljXSmM&code_challenge_method=S256

***** headers *****
{'Content-Type': 'application/x-www-form-urlencoded'}

***** body *****

127.0.0.1 - - [05/Apr/2023 16:09:49] "GET /?code=N9Y2DT77QNd_d1mmnsSjwUiqbxOvDB3AfT9QM5mD6XFuYYByhtSD4AfIjjWGF6AFTwdhA8QyOjfX63RO1mYsvw&state=aiq21soYtKWlzomgChYUlzVgjpby1v HTTP/1.1" 200 -

***** decoded access token *****
{'alg': 'RS512', 'kid': '3', 'typ': 'JWT'}
{   'aud': [   'https://webgw.localdomain/irisrsc/csp/myrsc',
               'https://webgw.localdomain/irisrsc2/csp/myrsc',
               'EKj6Bww0yiFgeuYUxtkyzfge84-keMy20uE5xTmlWPk'],
               ・
               ・
               ・

ユーザセッション(CSPOAuth2Sessionクッキー)が有効になっているため、最後にログアウトを実行しています。新しいタブが開き、空白のページが表示されますので、閉じてください。

  # Logout 
  logout_url='https://webgw.localdomain/irisauth/authserver/oauth2/logout?id_token_hint='+id_token+'&post_logout_redirect_uri=http://localhost:8080/fclogout&state='+state
  open_new(logout_url)

ここをコメントアウトすれば、次回以降の認証時にはログイン画面がスキップされます。

よくあるエラー

urllib.error.URLError: <urlopen error [WinError 10061] 対象のコンピューターによって拒否されたため、接続できませんでした 。>

サーバ環境が起動していない、あるいは到達できない場合に発生します。

Unexpected request - client_id

ブラウザに上記の内容が表示される場合、credentials.jsonが正しくコピーされていません。
このエラーが発生すると、DOSがControl-Cを受け付けない状態で止まってしまいます。その場合、ブラウザで別タブを開いて http://localhost:8080/ を開いてください(何も表示されません)。これでpythonコードが完了するはずです。

  File "F:\ISJ\WorkAtHome\git\python-oauth2\httpserverhandler.py", line 32, in get_access_token_from_url
    context.load_verify_locations(os.environ['REQUESTS_CA_BUNDLE'])
FileNotFoundError: [Errno 2] No such file or directory

証明書のチェーン(all.crt)がc:\temp\にコピーされていません。

今後の修正

今後のドキュメントへの加筆・修正等はこちらで行います。

curl

導入手順

不要です。

本来Windowsで実行するべき内容ですが、サーバの修正時に使用することが大半なので、ここではLinuxで実行しています。そのため、Linuxの/etc/hostsにも下記を追加しています。

127.0.0.1       webgw.localdomain

認可サーバには以下の設定が行われています。

  1. 認可サーバの構成で「リソース所有者のパスワード認証情報」がチェックされている

  2. curlがクライアントとして登録されている

クライアントの種別:機密
リダイレクト URL:  http://localhost:8080/  (実際には使用されないので何でも良いです)
サポートする許可タイプ (少なくともひとつをチェック):「リソース所有者のパスワード認証情報」をチェック
サポートされている応答タイプ(少なくとも1つをチェックする): 「コード」をチェック

起動方法

下記は、curl.shの実行結果です。

$ pwd
/home/irismeister/git/iris-oauth2
$ ./curl.sh

{
  "sub": "_SYSTEM"
}

{
  "HostName": "irisrsc",
  "UserName": "UnknownUser",
  "sub": "_SYSTEM",
  "aud": [
    "https://webgw.localdomain/irisrsc/csp/myrsc",
    "https://webgw.localdomain/irisrsc2/csp/myrsc",
    ""
  ],
  "sigalg": "RS512",
  "Status": "OK",
  "TimeStamp": "04/04/2023 17:25:57",
  "exp": "1680600357(2023-04-04 18:25:57)",
  "debug": {
    "jti": "https://webgw.localdomain/irisauth/authserver/oauth2.A20EIIXeenDpM6oK_At6ZEtCcKY",
    "iss": "https://webgw.localdomain/irisauth/authserver/oauth2",
    "sub": "_SYSTEM",
    "exp": 1680600357,
    "aud": [
      "https://webgw.localdomain/irisrsc/csp/myrsc",
      "https://webgw.localdomain/irisrsc2/csp/myrsc",
      ""
    ],
    "scope": "openid profile scope1 scope2",
    "iat": 1680596757,
    "customer_id": "RSC-00001"
  }
}

その他の使い道

アクセストークンはBearerトークンなので、他の方法で取得したアクセストークンを使用してリソースサーバにアクセス出来ます。
どのRPを使っても構わないのですが、下記のように実行すれば、正当なアクセスとみなされます。

access_token="eyJ0eXAiO.....BlohDZw" # 表示されたアクセストークンをコピペして環境変数に設定
curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' \
https://webgw.localdomain/irisrsc/csp/myrsc/private -s | jq

このURLはサーバ編でアクセスを拒否('NoAccessToken'エラー)させるために使用したものと同じです。

アクセストークンの秘匿性を保つことがいかに重要であるかを示す例ですが、開発時にはリソースサーバの簡単なデバッグツールとして使用できます。

0
0 436
記事 Tomohiro Iwamoto · 4月 3, 2023 31m read

本記事は、あくまで執筆者の見解であり、インターシステムズの公式なドキュメントではありません。

IRISのoAuth2機能関連の情報発信は既に多数ありますが、本稿では

  • 手順(ほぼ)ゼロでひとまず動作させてみる
  • 設定の見通しを良くするために、役割ごとにサーバを分ける
  • 目に見えない動作を確認する
  • クライアント実装(PythonやAngular,CSPアプリケーション等)と合わせて理解する
  • シングルサインオン/シングルログアウトを実現する

ということを主眼においています。

コミュニティ版で動作しますので、「とりあえず動かす」の手順に従って、どなたでもお試しいただけます。

現状、使用IRISバージョンはIRIS 2023.1のプレビュー版になっていますが、ソースコードは適宜変更します。

手順に沿ってコンテナを起動すると下記の環境が用意されます。この環境を使用して動作を確認します。

ユーザエージェント(ブラウザ)やPython/curlからのアクセスは、全てApache (https://webgw.localdomain/) 経由になります。青枠の中のirisclient等の文字はコンテナ名(ホスト名)です。

例えば、irisclientホストの/csp/user/MyApp.Login.clsにアクセスする場合、URLとして

 https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls

と指定します。

つまり、各エンドポイントは同一のorigin (https://webgw.localdomain) を持ちます。そのため、クロスサイト固有の課題は存在しません(カバーされません)が、仮に各サーバが別のドメインに存在しても基本的には動作するはずです。

oAuth2/OIDC(OpenID Connect)の利用シーンは多種多様です。

本例は、認証・認可サーバ,クライアントアプリケーション,リソースサーバの全てがIRISで実行されるクローズドな環境(社内や組織内での使用)を想定して、認可コードフロー(Authorization Code Flow)を実現します。分かりやすい解説が、ネットにたくさんありますので、コードフロー自身の説明は本稿では行いません。

認証・認可サーバの候補はIRIS, WindowsAD, Azure AD, AWS Cognito, Google Workspace, keycloak, OpenAMなどがあり得ます。個別に動作検証が必要です。

クライアントアプリケーション(RP)は、昨今はSPAが第一候補となると思いますが、利用環境によっては、SPA固有のセキュリティ課題に直面します。

IRISには、Confidential Clientである、従来型のWebアプリケーション(フォームをSubmitして、画面を都度再描画するタイプのWebアプリケーション)用のoAuth2関連のAPI群が用意されています。

そこで、Webアプリケーション(CSP)を選択することも考えられますが、クライアント編では、よりセキュアとされるSPA+BFF(Backend For Frontend)の構成を実現するにあたり、Webアプリケーション用APIをそのまま活用する方法をご紹介する予定です。

以下、サーバ編の動作確認には、CSPアプリケーションを使用しています。これは、新規開発にCSP(サーバページ)を使用しましょう、という事ではなく、BFF実現のために必要となる機能を理解するためです。BFFについては、クライアント編で触れます。BFFについては、こちらの説明がわかりやすかったです。

リソースサーバの役割はデータプラットフォームであるIRISは最適な選択肢です。医療系用のサーバ機能ですがFHIRリポジトリはその良い例です。本例では、至極簡単な情報を返すAPIを使用しています。

少しの努力でFHIRリポジトリを組み込むことも可能です。

サーバ編とクライアント編に分けて記載します。今回はサーバ編です。

とはいえ、クライアントとサーバが協調動作する仕組みですので、境界は少しあいまいです


使用環境

  • Windows10
    ブラウザ(Chrome使用)、curl及びpythonサンプルコードを実行する環境です。

  • Liunx (Ubuntu)
    IRIS, WebGateway(Apache)を実行する環境です。Windows10上のwsl2、仮想マシンあるいはクラウドで動作させる事を想定しています。

参考までに私の環境は以下の通りです。


用途O/Sホストタイプ
クライアントPCWindows10 Pro物理ホスト
Linux環境ubuntu 22.04.1 LTS上記Windows10上のwsl2

Linux環境はVMでも動作します。VMのubuntuは、ubuntu-22.04-live-server-amd64.iso等を使用して、最低限のサーバ機能のみをインストールしてあれば十分です。

Linux上に必要なソフトウェア

実行にはjq,openssl,dockerが必要です。 私の環境は以下の通りです。

$ jq --version
jq-1.6
$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
$ docker version
Client: Docker Engine - Community
 Version:           23.0.1

とりあえず動かす

下記手順でとりあえず動かしてみることが出来ます。

  • 以下は、Linuxで実行します。

    git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
    cd iris-oauth2
    ./first-run.sh
    

    この時点で下記をLinuxで実行し、OpenIDプロバイダーのメタデータを取得できる事を確認してください。こちらのような出力が得られるはずです。

    curl http://localhost/irisauth/authserver/oauth2/.well-known/openid-configuration
    
  • 以下はWindowsで実行します。

    クライアントPC(Windows)にホスト名(webgw.localdomain)を認識させるために、%SystemRoot%\system32\drivers\etc\hostsに下記を追加します。

    wsl2使用でかつlocalhostForwarding=Trueに設定してある場合は下記のように設定します。

    127.0.0.1 webgw.localdomain 
    

    VM使用時は、LinuxのIPを指定します。

    192.168.11.48 webgw.localdomain 
    

    次に、httpsの設定が正しく機能しているか確認します。作成された証明書チェーンをWindows側のc:\tempにコピーします。

    cp ssl/web/all.crt /mnt/c/temp
    

    VMの場合は、scp等を使用してssl/web/all.crtを c:\temp\all.crtにコピーしてください。以後、WSL2のコマンドのみを例示します。

    PCからcurlでリソースサーバの認証なしのRESTエンドポイントにアクセスします。ユーザ指定(-u指定)していないことに注目してください。

    curl --cacert c:\temp\all.crt --ssl-no-revoke -X POST https://webgw.localdomain/irisrsc/csp/myrsc/public
    {"HostName":"irisrsc","UserName":"UnknownUser","sub":"","aud":"","Status":"OK","TimeStamp":"03/28/2023 17:39:17","exp":"(1970-01-01 09:00:00)","debug":{}}
    

    認証なしのRESTサービスですので成功するはずです。次にアクセストークン/IDトークンによる認証・認可チェック処理を施したエンドポイントにアクセスします。

    curl --cacert c:\temp\all.crt --ssl-no-revoke -X POST https://webgw.localdomain/irisrsc/csp/myrsc/private
    {
      "errors":[ {
                "code":5035,
                "domain":"%ObjectErrors",
                "error":"エラー #5035: 一般例外 名前 'NoAccessToken' コード '5001' データ ''",
                "id":"GeneralException",
                "params":["NoAccessToken",5001,""
                ]
              }
      ],
      "summary":"エラー #5035: 一般例外 名前 'NoAccessToken' コード '5001' データ ''"
    }
    

    こちらは、期待通りエラーで終了します。

    次に、ブラウザでCSPベースのWEBクライアントアプリケーションを開きます。

    プライベート認証局発行のサーバ証明書を使用しているため、初回はブラウザで「この接続ではプライバシーが保護されません」といったセキュリティ警告が出ます。アクセスを許可してください。

    「oAuth2認証を行う」ボタンを押した際に、ユーザ名、パスワードを求められますので、ここではtest/testを使用してください。

    権限の要求画面で「許可」を押すと各種情報が表示されます。

    ページ先頭に「ログアウト(SSO)」というリンクがありますので、クリックしてください。最初のページに戻ります。

    IRISコミュニティエディションで、接続数上限に達してしまうと、それ以後は[Service Unavailable]になったり、認証後のページ遷移が失敗したりしますので、ご注意ください。その場合、下記のような警告メッセージがログされます。

    docker compose logs irisclient
    
    iris-oauth2-irisclient-1  | 03/24/23-17:14:34:429 (1201) 2 [Generic.Event] License limit exceeded 1 times since instance start.
    

    しばらく(10分ほど)待つか、終了・起動をしてください。

  • 以下は、Linuxで実行します。

    終了させるには下記を実行します。

    ./down.sh
    

主要エンドポイント一覧

下図は、コード認可フローを例にした、各要素の役割になります。用語としてはoAuth2を採用しています。

OIDCはoAuth2の仕組みに認証機能を載せたものなので、各要素は重複しますが異なる名称(Authorization serverはOIDC用語ではOP)で呼ばれています。

CLIENT SERVERという表現は「何どっち?」と思われる方もおられると思いますが、Client's backend serverの事で、サーバサイドに配置されるロジック処理機能を備えたWebサーバの事です。描画を担うJavaScriptなどで記述されたClient's frontendと合わせて単にClientと呼ぶこともあります。


要素サービス名OIDC用語oAuth2用語エンドポイント
ユーザエージェントN/AUser AgentUser AgentN/A
Web GatewaywebgwN/AN/A/csp/bin/Systems/Module.cxw
認可サーバの管理irisauthN/AN/A/irisauth/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#1の管理irisrscN/AN/Airisrsc/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#1irisrscN/AResource server/irisrsc/csp/myrsc/private
リソースサーバ#2の管理irisrsc2N/AN/A/irisrsc2/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#2irisrsc2N/AResource server/irisrsc2/csp/myrsc/private
WebApp 1a,1bの管理irisclientN/AN/A/irisclient/csp/sys/%25CSP.Portal.Home.zen
WebApp 1airisclientRPClient server/irisclient/csp/user/MyApp.Login.cls
WebApp 1birisclientRPClient server/irisclient2/csp/user/MyApp.AppMain.cls
WebApp 2の管理irisclient2N/AN/A/irisclient2/csp/sys/%25CSP.Portal.Home.zen
WebApp 2irisclient2RPClient server/irisclient2/csp/user/MyApp.AppMain.cls

エンドポイントのオリジン(https://webgw.localdomain)は省略しています


組み込みのIRISユーザ(SuperUser,_SYSTEM等)のパスワードは、merge1.cpfのPasswordHashで一括で"SYS"に設定しています。管理ポータルへのログイン時に使用します。

導入手順の解説

first-run.shは、2~5を行っています。

  1. ソースコード入手

    git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
    
  2. SSL証明書を作成

    ./create_cert_keys.sh
    

    apache-sslに同梱のsetup.shを使って、鍵ペアを作成し、出来たsslフォルダの中身を丸ごと、ssl/web下等にコピーしています。コピー先と用途は以下の通りです。

    コピー先使用場所用途
    ssl/web/ApacheのSSL設定およびクライアントアプリ(python)Apacheとのhttps通信用
    irisauth/ssl/auth/認可サーバ認可サーバのクライアント証明書
    irisclient/ssl/client/CSPアプリケーション#1a,1bIRIS(CSP)がクライアントアプリになる際のクライアント証明書
    irisclient2/ssl/client/CSPアプリケーション#2IRIS(CSP)がクライアントアプリになる際のクライアント証明書
    irisrsc/ssl/resserver/リソースサーバリソースサーバのクライアント証明書
    irisrsc2/ssl/resserver/リソースサーバ#2リソースサーバのクライアント証明書
  3. PCにクライアント用の証明書チェーンをコピー

    all.crtには、サーバ証明書、中間認証局、ルート認証局の情報が含まれています。curlやpythonなどを使用する場合、これらを指定しないとSSL/TLSサーバ証明書の検証に失敗します。

    cp ssl/web/all.crt /mnt/c/temp
    

    備忘録

    下記のコマンドで内容を確認できます。

    openssl crl2pkcs7 -nocrl -certfile ssl/web/all.crt | openssl pkcs7 -print_certs -text -noout
    
  4. Web Gatewayの構成ファイルを上書きコピー

    cp webgateway* iris-webgateway-example/
    
  5. コンテナイメージをビルドする

    ./build.sh
    

各種セットアップは、各サービス用のDockerfile以下に全てスクリプト化されています。iris関連のサービスは、原則、##class(MyApps.Installer).setup()で設定を行い、必要に応じてアプリケーションコードをインポートするという動作を踏襲しています。例えば、認可サーバの設定はこちらのDockefileと、インストーラ用のクラスであるMyApps.Installer(内容は後述します)を使用しています。

  1. ブラウザ、つまりクライアントPC(Windows)にホスト名webgw.localdomainを認識させる

    上述の通りです。

起動方法

./up.sh

up時に表示される下記のようなjsonは、後々、pythonなどの非IRISベースのクライアントからのアクセス時に使用する事を想定しています。各々client/下に保存されます。

{
  "client_id": "trwAtbo5DKYBqpjwaBu9NnkQeP4PiNUgnbWU4YUVg_c",
  "client_secret": "PeDUMmFKq3WoCfNfi50J6DnKH9KlTM6kHizLj1uAPqDzh5iPItU342wPvUbXp2tOwhrTCKolpg2u1IarEVFImw",
  "issuer_uri": "https://webgw.localdomain/irisauth/authserver/oauth2"
}

コンテナ起動後、ブラウザで下記(CSPアプリケーション)を開く。
https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls

停止方法

./down.sh  

認可サーバの設定について

カスタマイズ内容

多様なユースケースに対応するために、認可サーバの動作をカスタマイズする機能を提供しています。

特に、%OAuth2.Server.Authenticateはプロダクションには適さない可能性が高いのでなんらかのカスタマイズを行うように注記されていますのでご注意ください。

本例では、認証関連で下記の独自クラスを採用しています。

  • 認証クラス

    %ZOAuth2.Server.MyAuthenticate.cls

    下記を実装しています。
    BeforeAuthenticate() — 必要に応じてこのメソッドを実装し、認証の前にカスタム処理を実行します。

    ドキュメントに下記の記載があります。本例ではscope2が要求された場合には、応答に必ずscope99も含める処理を行っています。

    通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。

    AfterAuthenticate() — 必要に応じてこのメソッドを実装し、認証の後にカスタム処理を実行します。

    ドキュメントに下記の記載があります。本例ではトークンエンドポイントからの応答にaccountno=12345というプロパティを付与する処理を行っています。

    通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。

    トークンエンドポイントからの応答はリダイレクトの関係でブラウザのDevToolでは確認できません。pythonクライアントで表示出来ます。

    {   'access_token': '...........',
        'accountno': '12345',
        'expires_at': 1680157346.845698,
        'expires_in': 3600,
        'id_token': '...........',
        'refresh_token': '..........',
        'scope': ['openid', 'profile', 'scope1', 'scope2', 'scope99'],
        'token_type': 'bearer'
    }
    
  • ユーザクラスを検証(ユーザの検証を行うクラス)

    %ZOAuth2.Server.MyValidate.cls

    下記を実装しています。
    ValidateUser() — (クライアント資格情報を除くすべての付与タイプで使用)

    ここでは、トークンに含まれる"aud"クレームのデフォルト値を変更したり、カスタムクレーム(customer_id)を含める処理を行っています。

    {
      "jti":"https://webgw.localdomain/irisauth/authserver/oauth2.UQK89uY7wBdysNvG-fFh44AxFu8",
      "iss":"https://webgw.localdomain/irisauth/authserver/oauth2",
      "sub":"test",
      "exp":1680156948,
      "aud":[
        "https://webgw.localdomain/irisrsc/csp/myrsc",
        "https://webgw.localdomain/irisrsc2/csp/myrsc",
        "pZXxYLRaP8vAOjmMetLe1jBIKl0wu4ehCIA8sN7Wr-Q"
      ],
      "scope":"openid profile scope1",
      "iat":1680153348,
      "customer_id":"RSC-00001",
      "email":"test@examples.com",
      "phone_number":"01234567"
    }
    

これらの独自クラスは、下記で設定しています。

リフレッシュトークン

「パブリッククライアント更新を許可」をオンにしています。

この設定をオンにすると、client_secretを含まない(つまりpublic clientの要件を満たすクライアント)からのリフレッシュトークンフローを受け付けます。そもそもPublic Clientにはリフレッシュトークンを発行しない、という選択もありますが、ここでは許可しています。

また、「リフレッシュ・トークンを返す」項目で「常にリフレッシュトークンを返す」を設定しています。

「scopeに"offline_access"が含まれている場合のみ」のように、より強めの制約を課すことも可能ですが、今回は無条件に返しています

ユーザセッションをサポート

認可サーバの"ユーザセッションをサポート"を有効に設定しています。この機能により、シングルサインオン(SSO)、シングルログアウト(SLO)が実現します。

ユーザセッションをユーザエージェントとRP間のセッション維持に使用する"セッション"と混同しないよう

この設定を有効にすると、認可時に使用したユーザエージェントは、以後、ユーザ名・パスワードの再入力を求めることなくユーザを認証します。以下のように動作を確認できます。

  1. CSPベースのアプリケーション#1aをブラウザで開きます。ユーザ名・パスワードを入力し、認証を行います。

  2. 同じブラウザの別タブで、異なるclient_idを持つCSPベースのアプリケーション#1bを開きます。本来であれば、ユーザ名・パスワード入力を求められますが、今回はその工程はスキップされます。

  3. 上記はほぼ同じ表示内容ですが$NAMESPACE(つまり実行されているアプリケーション)が異なります。

アプリケーションが最初に認可されたスコープと異なるスコープを要求した場合、以下のようなスコープ確認画面だけが表示されます。

この時点で認可サーバで下記を実行すると、現在1個のセッションに属する(同じGroupIdを持つ)トークンが2個存在することが確認できます。

$ docker compose exec irisauth iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"
[SQL]%SYS>>SELECT * FROM OAuth2_Server.Session

ID                                              AuthTime        Cookie                                          Expires         Scope                   Username
6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60     1679909215      6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60     1679995615      openid profile scope1   test

[SQL]%SYS>>SELECT ClientId, GroupId,  Scope, Username FROM OAuth2_Server.AccessToken

ClientId                                        GroupId                                         Scope                   Username
qCIoFRl1jtO0KpLlCrfYb8TelYcy_G1sXW_vav_osYU     6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60     openid profile scope1   test
vBv3V0_tS3XEO5O15BLGOgORwk-xYlEGQA-48Do9JB8     6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60     openid profile scope1   test
  1. 両方のタブでF5を何度か押して、%session.Data("COUNTER")の値が増えて行くことを確認します。

セッションを持つアプリケーションの動作という見立てです。

  1. 1個目のタブ(CSPベースのアプリケーション#1a)でログアウト(SSO)をクリックします。ログアウトが実行され、最初のページに戻ります。

  2. 2個目のタブ(CSPベースのアプリケーション#1b)でF5を押します。「認証されていません! 認証を行う」と表示されます。

これで、1度のログアウト操作で、全てのアプリケーションからログアウトするSLOが動作したことがが確認できました。

同様に、サンプルのpythonコードも、一度認証を行うと、それ以降、何度実行してもユーザ名・パスワード入力を求めることはありません。これはpythonが利用するブラウザに"ユーザセッション"が記録されるためです。

          redirect
  <-----------------------------------
                                     |
python +--> ブラウザ           --> 認可サーバ
       |    (ユーザセッション)  
       +--> リソースサーバ

この設定が有効の場合、認可サーバはユーザエージェントに対してCSPOAuth2Sessionという名称のクッキーをhttpOnly, Secure設定で送信します。以後、同ユーザエージェントが認可リクエストを行う際には、このクッキーが使用され、(認可サーバでのチェックを経て)ユーザを認証済みとします。

CSPOAuth2Sessionの値は、発行されるIDトークンの"sid"クレームに含まれます。

{
  "iss":"https://webgw.localdomain/irisauth/authserver/oauth2",
  "sub":"test",
  "exp":1679629322,
  "auth_time":1679625721,
  "iat":1679625722,
  "nonce":"M79MJF6HqHHDKFpK4ZZJkaD3moE",
  "at_hash":"AFeWfbXALP78Y9KEhlKnp_5LJmEjthJQlJDGXh_eLPc",
  "aud":[
    "https://webgw.localdomain/irisrsc/csp/myrsc",
    "https://webgw.localdomain/irisrsc2/csp/myrsc",
    "SrGSiVPB8qWvQng-N7HV9lYUi5WWW_iscvCvGwXWGJM"
  ],
  "azp":"SrGSiVPB8qWvQng-N7HV9lYUi5WWW_iscvCvGwXWGJM",
  "sid":"yxGBivVOuMZGr2m3Z5AkScNueppl8Js_5cz2KvVt6dU"
}

詳細はこちら の「ユーザ・セッションのサポート」の項目を参照ください。

PKCE

認可コード横取り攻撃への対策である、PKCE(ピクシーと発音するそうです)関連の設定を行っています。そのため、PublicクライアントはPKCEを実装する必要があります。

  • 公開クライアントにコード交換用 Proof Key (PKCE) を適用する: 有効
  • 機密クライアントにコード交換用 Proof Key (PKCE) を適用する: 無効

ログアウト機能

OpenID Connectのログアウト機能について再確認しておきます。

実に、様々なログアウト方法が提案されています。メカニズムとして、postMessage-Based Logout,HTTP-Based Logoutがあり、ログアウト実行の起点によりRP-Initiated, OP-Initiatedがあり、さらにHTTP-Based LogoutはFront-Channel, Back-Channelがありと、利用環境に応じて様々な方法が存在します。

postMessageとはクロスドメインのiframe間でデータ交換する仕組みです

目的は同じでシングルログアウト(SLO)、つまり、シングルサインオンの逆で、OP,RP双方からログアウトする機能を実現することです。

本例での設定

HTTP-Basedを使用したほうがクライアント実装が簡単になる事、バックチャネルログアウトは現在IRISでは未対応であることから、本例では、フロントチャネルログアウトをRP-Initiatedで実行しています。

ユーザセッションが有効なクライアント(irisclient)のログアウト用のリンクをクリックすると下記のようなJavaScriptを含むページが描画されます。

<body onload="check(0)">
<iframe id=frame0 src=RP1のfrontchannel_logout_uri hidden></iframe>
<iframe id=frame1 src=RP2のfrontchannel_logout_uri hidden></iframe>
  ・
  ・
<scr1pt language=javascript type="text/javascript">
    function check(start) {
      個々のiframeの実行完了待ち
      if (完了) doRedirect()
    }
    function doRedirect() {
            post_logout_redirect_uriへのリダイレクト処理
    }
</scr1pt>

表示がおかしくなってしまうので、scriptをscr1ptに変更しています。インジェクション攻撃扱いされています...?

JavaScriptが行っていることは、iframe hiddenで指定された各RPログアウト用のエンドポイント(複数のRPにログインしている場合、iframeも複数出来ます)を全て呼び出して、成功したら、doRedirect()で、post_logout_redirect_urisで指定されたURLにリダイレクトする、という処理です。これにより、一度の操作で全RPからのログアウトとOPからのログアウト、ログアウト後の指定したページ(本例では最初のページ)への遷移が実現します。

内容を確認したい場合、ログアウトする前に、ログアウト用のリンクのURLをcurlで実行してみてください。

curl -L --insecure "https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls?register=R3_wD-F5..."

一方、ユーザセッションが無効の場合は、ログアウトを実行したクライアントのみがfrontchannel_logout対象となります。

つまり、ユーザセッションを使用して、2回目以降にユーザ名・パスワードの入力なしで、認証されたアプリケーション群が、SLOでログアウトされる対象となります。

フロントチャネルログアウト実現のために、認可サーバの設定で、下記のログアウト関連の設定を行っています。

  • HTTPベースのフロントチャネルログアウトをサポート:有効
  • フロントチャネルログアウトURLとともに sid (セッションID) クレームの送信をサポート:有効

また、認可サーバ(irisauth)に以下のcookie関連の設定を行っています。

ドキュメントに従って、irisauthの/oauth2のUser Cookie Scopeをlaxとしています。

Note: For an InterSystems IRIS authorization server to support front channel logout, the User Cookie Scope for the /oauth2 web application must be set to Lax. For details on configuring application settings, see Create and Edit Applications.

本例は同じオリジンで完結している(Chromeであれば、ログアウト実行時に関わるhttpアクセスのRequest Headersに含まれるsec-fetch-site値がsame-originになっていることで確認できます)ので、この設定は不要ですが、備忘目的で設定しています。

また、クライアント(irisclient)に以下の設定を行っています。

  1. Session Cookie Scopeの設定

ドキュメントに従って、irisclientの/csp/userのSession Cookie Scopeをnoneとしています。

Note: For an InterSystems IRIS client to support front channel logout, the Session Cookie Scope of the client application to None. For details on configuring application settings, see Create and Edit Applications.

本例は同じオリジンで完結している(Chromeであれば、ログアウト実行時に関わるhttpアクセスのRequest Headersに含まれるsec-fetch-site値がsame-originになっていることで確認できます)ので、この設定は不要ですが、備忘目的で設定しています。

  1. "frontchannel_logout_session_required"をTrueに設定しています。

  2. "frontchannel_logout_uri"に"https://webgw.localdomain/irisclient/csp/user/MyApp.Logout.cls"を設定しています。

管理ポータル上には下記のように表示されています。このURLに遷移する際は、IRISLogout=endが自動付与されます。

If the front channel logout URL is empty, the client won't support front channel logout. 'IRISLogout=end' will always be appended to any provided URL.

IRISドキュメントに記述はありませんが、Cache'の同等機能の記述はこちらです。IRISLogout=endは、CSPセッション情報の破棄を確実なものとするためと理解しておけば良いでしょう。

一般論として、ログアウト時のRP側での処理は認可サーバ側では制御不可能です。本当にセッションやトークンを破棄しているか知るすべがありません。IRISLogout=endはRPがCSPベースである場合に限り、それら(cspセッションとそれに紐づくセッションデータ)の破棄を強制するものです。非CSPベースのRPにとっては意味を持ちませんので、無視してください。

各サーバの設定について

各サーバの設定内容とサーバ環境を自動作成する際に使用した各種インストールスクリプトに関する内容です。

認可サーバ

認可サーバ上の、oAUth2/OIDC関連の設定は、MyApps.Installerにスクリプト化してあります。

下記の箇所で、「OAuth 2.0 認可サーバ構成」を行っています。

Set cnf=##class(OAuth2.Server.Configuration).%New()
  ・
  ・
Set tSC=cnf.%Save()

これらの設定は、認可サーバで確認できます。

認可サーバ上のクライアントデスクリプション

下記のような箇所が3か所あります。これらは「 OAuth 2.0 サーバ クライアントデスクリプション」で定義されている、python, curl, angularのエントリに相当します。

Set c=##class(OAuth2.Server.Client).%New()
Set c.Name = "python"
  ・
  ・
Set tSC=c.%Save()

これらに続くファイル操作は、利便性のためにclient_idなどをファイル出力しているだけで、本来は不要な処理です。

これらはコンテナイメージのビルド時に実行されます。

これらの設定は、認可サーバで確認できます。

CSPベースのWebアプリケーション

実行内容の説明は、クライアント編で行います。

CSPベースのWebアプリケーションの設定は、MyApps.Installerにスクリプト化してあります。

oAUth2/OIDC関連の設定(クライアントの動的登録)は、irisclient用のRegisterAll.mac、およびirisclient2用のRegisterAll.macにスクリプト化してあります。

これらはregister_oauth2_client.shにより、コンテナ起動後に実行されます。

これらの設定は、クライアント用サーバで確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。

ビルド時に生成されるclient_idがURLに含まれるため、リンクを用意できません。画像イメージのみです。

リソースサーバ

リソースサーバの設定は、MyApps.Installerにスクリプト化してあります。

リソースサーバのRESTサービスは、IRISユーザUnknownUserで動作しています。

リソースサーバは、受信したトークンのバリデーションをするために、REST APIの実装で、下記のAPIを使用しています。

アクセストークンをhttp requestから取得します。

set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.tSC)

アクセストークンのバリデーションを実行します。この際、..#AUDがアクセストークンのaudクレームに含まれていることをチェックしています。

if '(##class(%SYS.OAuth2.Validation).ValidateJWT($$$APP,accessToken,,..#AUD,.jsonObjectJWT,.securityParameters,.tSC)) {

署名の有無の確認をしています。

Set sigalg=$G(securityParameters("sigalg"))
if sigalg="" { 
  set reason=..#HTTP401UNAUTHORIZED
  $$$ThrowOnError(tSC)				
}

(べた書きしていますが)受信したアクセストークンのSCOPEクレーム値がscope1を含まない場合、http 404エラーを返しています。

if '(jsonObjectJWT.scope_" "["scope1 ") { set reason=..#HTTP404NOTFOUND throw }

oAUth2/OIDC関連の設定(クライアントの動的登録)は、Register.macにスクリプト化してあります。

これらはregister_oauth2_client.shにより、コンテナ起動後に実行されます。

これらの設定は、リソースサーバで確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。

ビルド時に生成されるclient_idがURLに含まれるため、リンクを用意できません。画像イメージのみです。

署名(JWK)

認可サーバをセットアップすると、一連の暗号鍵ペアが作成されます。これらはJWTで表現されたアクセストークンやIDトークンを署名する(JWS)ために使用されます。

鍵情報は認可サーバのデータべースに保存されています。参照するにはirisauthで下記SQLを実行します。

$ docker compose exec irisauth iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"

SELECT PrivateJWKS,PublicJWKS FROM OAuth2_Server.Configuration

PrivateJWKSの内容だけを見やすいように整形するとこちらのようになります。

実際にアクセストークンを https://jwt.io/ で確認してみます。ヘッダにはkidというクレームが含まれます。これはトークンの署名に使用されたキーのIDです。

{
  "typ": "JWT",
  "alg": "RS512",
  "kid": "3"
}

これで、このトークンはkid:3で署名されていることがわかります。 この時点で、Signature Verifiedと表示されていますが、これはkid:3の公開鍵を使用して署名の確認がとれたことを示しています。

公開鍵は公開エンドポイントから取得されています

次に、エンコード処理(データへのJWSの付与)を確認するために、ペーストしたトークンの水色の部分(直前のピリオドも)をカットします。Invalid Signatureに変わります。

さきほどSQLで表示したPrivateJWKSの内容のkid:3の部分だけ(下記のような内容)を抜き出して下のBOXにペーストします。

{
  "kty": "RSA",
  "n": "....",
  "e": "....",
  "d": "....",
    ・
    ・
    ・
  "alg": "RS512",
  "kid": "3"
}

水色部分が復元され、再度、Signature Verifiedと表示されるはずです。また、水色部分は元々ペーストしたアクセストークンのものと一致しているはずです。

本当に大切な秘密鍵はこういう外部サイトには張り付けないほうが無難かも、です

ログ取得方法

各所でのログの取得方法です。

認可サーバ(IRIS)

認可サーバ上の実行ログを取得、参照出来ます。クライアントの要求が失敗した際、多くの場合、クライアントが知りえるのはhttpのステータスコードのみで、その理由は明示されません。認可サーバ(RPがIRISベースの場合は、クライアントサーバでも)でログを取得すれば、予期せぬ動作が発生した際に、原因のヒントを得ることができます。

  • ログ取得開始

    ./log_start.sh
    

    これ以降、発生した操作に対するログが保存されます。ログは^ISCLOGグローバルに保存されます。

  • ログを出力

    ログは非常に多くなるので、いったんファイルに出力してIDE等で参照するのが良いです。

    ./log_display.sh
    

    Webアプリケーション1aをユーザエージェント(ブラウザ)からアクセスした際のログファイルの出力例はこちらです。

  • ログを削除
    ログを削除します。ログ取得は継続します。

    ./log_clear.sh
    
  • ログ取得停止 ログ取得を停止します。ログ(^ISCLOGグローバル)は削除されません。

    ./log_end.sh
    

IRISサーバのログ確認

IRISサーバが稼働しているサービス名(認可サーバならirisauth)を指定します。

IRISコミュニティエディション使用時に、接続数オーバ等を発見できます。

docker compose logs -f irisauth

WebGWのログ確認

WebGWコンテナ内で稼働するapacheのログを確認できます。

全体の流れを追ったり、エラー箇所を発見するのに役立ちます。

docker compose logs -f webgw
0
1 570
記事 Tomohiro Iwamoto · 7月 11, 2022 2m read

オリジナルの「InterSystems IRIS で Python を使って IMAPクライアントを実装する」は、埋め込みPythonを使用してIMAPインバウンドアダプタを実装されていますが、最近メールプロバイダがあいついでoAuth2認証しか受け付けなくなってきているので、その対応をしてみました。

本稿のGitHubはこちらです。

変更点

GMAILに対してメールの送受信を可能とするためにオリジナルに以下の修正を施しています。

  1. IMAP(Python版)インバウンドアダプタにoAuth2認証およびRefreshTokenによるAccessTokenの更新を追加
  2. oAuth2認証およびRefreshTokenによるAccessTokenの更新機能を持つSMTPアウトバウンドアダプタを新規作成
  3. IMAPにバイナリの添付ファイルの処理を追加
  4. メッセージ削除に、推奨APIであるclient.uid("STORE")を使用するように変更
  5. ClientIdなど、センシティブな情報をコンテナ起動時に動的に適用するように変更
  6. 日本語使用時の文字化けに対処

3.添付ファイルが存在する場合、追加設定/ファイル・パスで指定したファイルパス(既定値は/var/tmp/)上に保存します。

5.の実現は、プロダクション(IMAPPyProduction.cls)起動の際に実行されるコールバックOnStart()で、準備したjsonファイルの取り込みを行っています。

zpmパッケージの内容はオリジナルのままです。

事前準備

実行には、以下のパラメータの準備が必要です。

パラメータ取得方法
GMAILアカウント認証対象となるGMAILアカウント。xxxx@gmail.com
ClientIDGCPで発行されるclient_id
ClientSecretGCPで発行されるclient_secret
TokenEndPointGCPで発行されるtoken_uri
RefreshToken下記のoauth2.py等を使用して取得

これらの値をgmail_client_secret.templateを参考に、gmail_client_secret.jsonに設定してください。

ClientID, ClientSecret, TokenEndPointは、GCPのコンソールで、デスクトップクライアント用にoAuth2を発行した際にダウンロードできるJSONファイルから取得すると便利です。

$ cat client_secret_xxxxxx.apps.googleusercontent.com.json | jq

{
  "installed": {
    "client_id": "xxxxx.apps.googleusercontent.com",  <=ココ
    "project_id": "iris-mail-355605",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",       <=ココ
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "yyyyy",    <=ココ
    "redirect_uris": [
      "http://localhost"
    ]
  }
}

Refresh Tokenの取得には、oauth2.pyを使用しました。

$ python2 oauth2.py --user=xxxx@gmail.com \
    --client_id=xxxxxx.apps.googleusercontent.com \
    --client_secret=GOCSPX-yyyyyyy  \
    --generate_oauth2_token

Refresh Token: xxxxxxx    <=ココ
Access Token: yyyyyyyyyyyyyyyy
Access Token Expiration Seconds: 3599

実行

オリジナルと同じで、下記コマンドを実行します。1つのビジネスサービス、1つのビジネスオペレーションを持つ簡単なプロダクションが起動します。

URLはオリジナルのままのhttp://localhost:52785/csp/sys/%25CSP.Portal.Home.zenにしてあります。

git clone https://github.com/IRISMeister/iris-imap-inbound-adapter-demo
cd iris-imap-inbound-adapter-demo
docker-compose build
docker-compose up -d

安全策として、IMAP-GMAIL, SMTP-GMAILはいずれもdisableにしてあります。それぞれのパラメータ設定が適切に適用されていることを確認の上、有効化してください。

image

ビジネスホストの設定値

ビジネスホスト名パラメータ名
IMAP-GMAILRefreshTokengmail_client_secret.json設定値
IMAP-GMAILClientIdgmail_client_secret.json設定値
IMAP-GMAILClientSecretgmail_client_secret.json設定値
IMAP-GMAIL認証情報mail-gmail
SMTP-GMAILRefreshTokengmail_client_secret.json設定値
SMTP-GMAILClientIdgmail_client_secret.json設定値
SMTP-GMAILClientSecretgmail_client_secret.json設定値
SMTP-GMAILTokenEndPointgmail_client_secret.json設定値
SMTP-GMAIL認証情報mail-gmail

以降、IMAP-GMAILサービスが30秒毎に件名に[IMAP test]を含むメールをチェックし、存在した場合、SMTP-GMAILオペレーションが自分自身に送信します。

同件名のメールが存在しないと、何も起こりません。下記で、そのようなメールを1通送信することが出来ます。

もちろん、通常のメールクライアントソフトウェアを使って、送信しても構いません

docker-compose exec iris iris session iris -U IRISAPP "Send"

プロダクションを停止しない限り、メールの送受信を延々と繰り返しますので、適当なタイミングで停止してください。

image

0
0 631
記事 Toshihiko Minamoto · 3月 17, 2022 5m read


この記事では、以下のオンラインデモを通じ、GitHub アカウントを使用した OAuth2 認証の基本を説明します。
https://dappsecurity.demo.community.intersystems.com/csp/user/index.csp(SuperUser | SYS)

推奨事項:

目的を達成するために必要な 3 つのステップ:

  • ステップ 1: GitHub 認証サーバーにアプリケーションを登録する
  • ステップ 2: InterSystems 管理ポータルから OAuth 2.0 クライアントを構成する
  • ステップ 3: GitHub アカウントでログインするための API を呼び出す

それでは始めましょう。 

ステップ 1: GitHub 認証サーバーにアプリケーションを登録する

GitHub 認証サーバーにアプリケーションを登録するには、GitHub アカウントが必要です。 
GitHub アカウントにログインして https://github.com/settings/developers に移動し、[OAuth Apps(OAuth アプリ)]タブで[New OAuth App(新しい OAuth アプリ)]ボタンをクリックします。

 

アプリケーション名、ホームページ URL、説明、および認証コールバック URL を入力します。
認証コールバック URL は OAuth2.Response.cls クラス({domain}/csp/sys/oauth2/OAuth2.Response.cls)を参照する必要があることに注意してください。
[Register Application(アプリケーションを登録)]をクリックします。

すると、詳細ページが開きます。 [Generate a new client secret(新しいクライアントシークレットを生成)]をクリックし、IRIS OAuth2 クライアントを構成する際に使用するクライアント ID と秘密鍵を保存します。
アプリケーションが登録されました。

ステップ 2: InterSystems 管理ポータルから OAuth 2.0 クライアントを構成する

システム]>[セキュリティ管理]>[OAuth 2.0 クライアント]に移動し、[サーバーの説明を作成]ボタンをクリックします。

上部にある[手動]ボタンをクリックし、以下の詳細を入力してサーバーの説明を保存します。

システム]>[セキュリティ管理]>[OAuth 2.0 クライアント]に戻り、[クライアント構成]をクリックします。

[一般]タブに詳細を入力します。 クライアントリダイレクト URL は、アプリケーションを GitHub に登録した際に入力した認証コールバック URLと同じであることに注意してください。

[クライアント資格情報]に、アプリケーションを GitHub に登録した際に生成したクライアント ID とクライアントシークレットを入力し、クライアント構成を保存します。

ステップ 3: GitHub アカウントでログインするための API を呼び出す

オンラインデモ(https://dappsecurity.demo.community.intersystems.com/csp/user/index.csp)に移動し、 SuperUser | SYS でログインします。
トップメニューから[Login with GitHub(GitHub でログイン)]をクリックします。


これで、Oauth.cls ページが開きます。 サインインを試す前に、必ず GitHub からログアウトしてください。 [GitHub Sign In(GitHub サインイン)]をクリックします。

システムは %SYS.OAuth2.Authorization クラス(https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&PRIVATE=1&CLASSNAME=%25SYS.OAuth2.Authorization)の GetAuthorizationCodeEndpoint メソッドを使用して、GitHub 認証サーバーに移動します。

ログインに成功すると、システムは OauthRe.cls ページにリダイレクトします。

コードは https://github.com/mwaseem75/Data_APP_Security のリポジトリから入手できます。

以上です!

0
0 374
記事 Toshihiko Minamoto · 8月 11, 2021 17m read

不在時に、セキュリティとプライバシーを維持しながら、コンピューターを相互に信頼させるにはどうすればよいでしょうか?

「ドライマルティーニを」と彼は言った。 「1 杯。 深いシャンパングラスで。」
「承知いたしました。」
「気が変わった。 ゴードンを 3、ヴォッカを 1、キナリレを半量。 キンキンに冷えるまでよくシェイクしてから、大きめの薄いレモンピールを 1 つ加えてくれ。 わかったかい?」
「お承りいたしました。」 バーテンダーはその考えが気に入ったようだった。
イアン・フレミング著『カジノ・ロワイヤル』(1953 年)より

OAuth は、ユーザーログイン情報を伴うサービスを「運用中」のデータベースから、物理的にも地理的にも分離する上で役立ちます。 このように分離すると、ID データの保護が強化され、必要であれば、諸国のデータ保護法の要件に準拠しやすくしてくれます。

OAuth を使用すると、ユーザーは、最小限の個人データをさまざまなサービスやアプリケーションに「公開」しながら、一度に複数のデバイスから安全に作業することができるようになります。 また、サービスのユーザーに関する「過剰な」データを操作しなくてよくなります(データはパーソナル化されていない形態で処理することができます)。

InterSystems IRIS を使用する場合、OAuth と OIDC サービスを自律的かつサードパーティのソフトウェア製品と連携してテストし、デプロイするための既成の完全なツールセットを利用できます。

OAuth 2.0 と Open ID Connect

OAuth と Open ID Connect(OIDC または OpenID)は、アクセスと識別をデリゲートするためにオープンプロトコルを汎用的に組み合わせたもので、21 世紀現在、人気を得ているようです。 大規模な使用において、これより優れたオプションはまだ誰も思いついていません。 HTTP(S) プロトコル上にとどまり、JWT(JSON Web Token)コンテナを使用するため、特にフロントエンドのエンジニアに人気があります。

OpenID は OAuth を使用して機能しています。実際、OpenID は OAuth のラッパーです。 OpenID を電子識別システムの認証と作成に使用するオープンスタンダードとして使用することは、開発者にとって目新しい事ではありません。 2019 年には、公開から 14 周年を迎えました(バージョン 3)。 Webとモバイル開発、そしてエンタープライズシステムで人気があります。

そのパートナーである OAuth オープンスタンダードはアクセスをデリゲートする役割を担っており、12 年目を迎えています。関連する RFC 5849 標準が登場してからは 9 年です。 この記事の目的により、プロトコルの最新バージョンである OAuth 2.0 と最新の RFC 6749 を使用することにしましょう。 (OAuth 2.0 は、その前身の OAuth 1.0 とは互換していません。)

厳密に言えば、OAuth はプロトコルではなく、ソフトウェアシステムにアクセス権制限アーキテクチャを実装する際に、ユーザー識別操作を分離して別のトラステッドサーバーに転送するための一連のルール(スキーム)です。

OAuth は特定のユーザーについて何も言及できないことに注意してください! ユーザーが誰であるか、ユーザーがどこにいるのか、またユーザーが現在コンピューターを使用しているかどうかさえも、知ることはできません。 ただし、OAuth を使用すれば、事前に発行されたアクセストークンを使用して、ユーザーが参加することなくシステムと対話することが可能であり、 これは重要なポイントです(詳細は、OAuth サイトにある「User Authentication with OAuth 2.0」をご覧ください)。

User-Managed Access(UMA)プロトコルも OAuth に基づくプロトコルです。 OAuth、OIDC、および UMA を合わせて使用することで、次のような分野で保護された ID とアクセス管理(IdM、IAM)システムを実装することができます。

API エコノミーの新しいアクセス制御ベン図
API エコノミーの新しいアクセス制御ベン図

何よりも、個人データをシステムのほかの部分と同じ場所に保存してはいけません。 認証と認可は物理的に分離する必要があります。 そして、ID と認証を各個人に与えることが理想と言えます。 自分で保管せずに、 所有者のデバイスを信頼するのです。

信頼と認証

ユーザーの個人データを自分のアプリや作業データベースと組み合わさったストレージ場所に保存するのはベストプラクティスではありません。 言い換えれば、このサービスを提供できる信頼のある人を選ぶようにする必要があります。

このサービスは、次の項目で構成されます。

  • ユーザー
  • クライアントアプリ
  • 識別サービス
  • リソースサーバー

アクションは、ユーザーのコンピューターの Web ブラウザで実行されます。 ユーザーには識別サービスが備わったアカウントがあり、 クライアントアプリは、識別サービスと相互インターフェースとの契約に署名済みです。 リソースサーバーは、識別サービスを信頼して、識別できた人にアクセスキーを発行します。

ユーザーはクライアント Web アプリを実行して、リソースを要求します。 クライアントアプリは、アクセス権が必要なそのリソースへのキーを提示する必要があります。
ユーザーにキーがない場合、クライアントアプリはリソースサーバーへのキーを発行するために契約している識別サービスに接続します(ユーザーを識別サービスに転送します)。

識別サービスは、どのようなキーが必要かを問い合わせます。

ユーザーは、リソースにアクセスするためのパスワードを入力します。 この時点でユーザー認証が行われ、ユーザーの身元が確認されると、リソースへのキーが提供され(ユーザーをクライアントアプリに戻します)、ユーザーがリソースを利用できるようになります。

認可サービスの実装

InterSystems IRIS プラットフォームでは、必要に応じてさまざまなプラットフォームからのサービスをアセンブルできます。 次はその例です。

  1. デモクライアントが登録された OAuth サーバーを構成して起動します。
  2. デモ OAuth クライアントを OAuth サーバーと Web リソースに関連付けて構成します。
  3. OAuth を使用できるクライアントアプリを開発します。 Java、Python、C#、または Node JS を使用できます。 以下の方に、ObjectScript でのアプリケーションコードの例を示しています。

OAuth にはさまざまな設定があるため、チェックリストが役立ちます。 例を見ていきましょう。 IRIS 管理ポータルに移動し、[システム管理]>[セキュリティ]>[OAuth 2.0]>[サーバー]の順に選択します。

各項目には設定行の名前とコロン、そして必要であればその後に例または説明が含まれます。 別の方法として、Daniel Kutac の 3 部構成になっている「InterSystems IRIS Open Authorization Framework (OAuth 2.0)の実装 - パート1」、パート2、そしてパート3 に記載されているスクリーンショットのヒントを参考にしてください。

次のスクリーンショットはすべて、例として提示されています。 独自のアプリケーションを作成する際は、別のオプションを選択する必要があるでしょう。

[一般設定]タブで、次のように設定してください。

  • 説明: 構成の説明を入力します。「認証サーバー」など。
  • ジェネレーターのエンドポイント(以降「EPG」)のホスト名: サーバーの DNS 名。
  • サポートされている許可の種類(少なくとも 1 つを選択):
    • 認可コード
    • 暗黙
    • アカウントの詳細: リソース、所有者、パスワード
    • クライアントアカウントの詳細
  • SSL/TLS 構成: oauthserver

[スコープ]タブで、次を設定します。

  • サポートされているスコープを追加: この例では「scope1」です。

[間隔]タブで、次を設定します。

  • アクセスキー間隔: 3600
  • 認可コードの間隔: 60
  • キー更新の間隔: 86400
  • セッション中断間隔: 86400
  • クライアントキー(クライアントシークレット)の有効期間: 0

[JWT 設定]タブで、次を設定します。

  • 入力アルゴリズム: RS512
  • キー管理アルゴリズム: RSA-OAEP
  • コンテンツ暗号化アルゴリズム: A256CBC-HS512

[カスタマイズ]タブで、次を設定します。

  • 識別クラス: %OAuth2.Server.Authenticate
  • ユーザークラスの確認: %OAuth2.Server.Validate
  • セッションサービスクラス: OAuth2.Server.Session
  • キーの生成クラス: %OAuth2.Server.JWT
  • カスタムネームスペース: %SYS
  • カスタマイズロール(少なくとも 1 つ選択): %DB_IRISSYS および %Manager

では、変更内容を保存します。

次のステップでは、OAuth サーバーにクライアントを登録します。 [顧客の説明]ボタンをクリックして、[顧客の説明を作成]をクリックします。

[一般設定]タブで、次の情報を入力します。

  • 名前: OAuthClient
  • 説明: 簡単な説明を入力します。
  • クライアントタイプ: 機密
  • リダイレクト URL: oauthclient から識別した後に、アプリに戻るポイントのアドレス。
  • サポートされている付与の種類:
    • 認可コード: はい
    • 暗黙
    • アカウントの詳細: リソース、所有者、パスワード
    • クライアントアカウントの詳細
    • JWT 認可
  • サポートされているレスポンスタイプ: 次のすべてを選択してください。
    • コード
    • id_token
    • id_token キー
    • トークン
  • 認可タイプ: シンプル

[クライアントアカウントの詳細]タブは自動的に入力されますが、クライアントの正しい情報であるかを確認してください。
[クライアント情報] タブには次の項目があります。

  • 認可画面:
    • クライアント名
    • ロゴの URL
    • クライアントのホームページ URL
    • ポリシーの URL
    • 利用規約の URL

では、[システム管理]>[セキュリティ]>[OAuth 2.0]>[クライアント]の順に移動して、OAuth サーバークライアントにバインディングを構成します。

サーバーの説明の作成:

  • ジェネレーターのエンドポイント: 一般的なサーバーのパラメーターから取得されます(上記を参照)。
  • SSL/TLS 構成: 事前構成済みのリストから選択します。
  • 認可サーバー:
    • 認可エンドポイント: EPG + /authorize
    • キーエンドポイント: EPG + /token
    • ユーザーエンドポイント: EPG + /userinfo
    • キーのセルフテストエンドポイント: EPG + /revocation
    • キーの終了エンドポイント: EPG + /introspection
  • JSON Web Token(JWT)設定:
    • 動的登録以外のほかのソース: URL から JWKS を選択します。
    • URL: EPG + /jwks

このリストから、たとえばサーバーが OAuth-client にユーザーに関するさまざまな情報を提供できることがわかります(scopes_supported および claims_supported)。 また、アプリケーションを実装するときは、共有する準備ができているデータが何であるかをユーザーに尋ねても何の価値もありません。 以下の例では、scope1 の許可のみを要求します。

では、構成を保存しましょう。

SSL 構成に関するエラーがある場合は、[設定]>[システム管理]>[セキュリティ]>[SSL/TSL 構成]に移動して、構成を削除してください。

これで OAuth クライアントをセットアップする準備が整いました。
[システム管理]>[セキュリティ]>[OAuth 2.0]>[クライアント]>[クライアント構成]>[クライアント構成を作成]に移動します。 [一般]タブで、次を設定します。

  • アプリケーション名: OAuthClient
  • クライアント名: OAuthClient
  • 説明: 説明を入力します。
  • 有効: はい
  • クライアントタイプ: 機密
  • SSL/TCL 構成: oauthclient を選択します。
  • クライアントリダイレクト URL: サーバーの DNS 名
  • 必要な許可の種類:
    • 認可コード: はい
    • 暗黙
    • アカウントの詳細: リソース、所有者、パスワード
    • クライアントアカウントの詳細
    • JWT 認可
  • 認可タイプ: シンプル

[クライアント情報]タブで、次を設定します。

  • 認可画面:
    • ロゴの URL
    • クライアントのホームページ URL
    • ポリシーの URL
    • 利用規約の URL
  • デフォルトのボリューム: サーバーに以前に指定したものが取得されます(scope1 など)。
  • 連絡先メールアドレス: カンマ区切りでアドレスを入力します。
  • デフォルトの最大経過時間(分): 最大認可経過時間または省略できます。

[JWT 設定]タブで、次を設定します。

  • JSON Web Token(JWT)設定
  • X509 アカウントの詳細から JWT 設定を作成する
  • IDToken アルゴリズム:
    • 署名: RS256
    • 暗号化: A256CBC
    • キー: RSA-OAEP
  • Userinfo アルゴリズム
  • アクセストークンアルゴリズム
  • クエリアルゴリズム

[クライアントログイン情報]タブで、次を設定します。

  • クライアント ID: クライアントがサーバーに登録された際に発行された ID(上記を参照)。
  • 発効されたクライアント ID: 入力されません
  • クライアントシークレット: クライアントがサーバーに登録された際に発行されたシークレット(上記を参照)。
  • クライアントシークレットの有効期限: 入力されません
  • クライアント登録 URI: 入力されません

構成を保存しましょう。

OAuth 認可を使用した Web アプリ

OAuth は、インタラクション参加体(サーバー、クライアント、Web アプリケーション、ユーザーのブラウザ、リソースサーバー)間の通信チャネルが何らかの形で保護されていることに依存しています。 この役割は SSL/TLS プロトコルが果たしているのがほとんどですが、 OAuth は、保護されていないチャネルでも機能します。 そのため、たとえばサーバー Keycloak はデフォルトで HTTP プロトコルを使用して、保護なしで実行します。 調整と調整時のデバッグが単純化されます。 サービスを実際に使用する際、OAuth のチャネル保護は、厳重な要件に含まれるべきであり、Keycloak ドキュメントに記述されている必要があります。 InterSystems IRIS の開発者は、OAuth に関するより厳密なアプローチに従っており、SSL/TSL の使用を要件としています。 単純化できる唯一の方法は、自己署名証明書を使用するか、組み込みの IRIS サービス PKI([システム管理]>>[セキュリティ]>>[公開鍵システム]を利用することです。

ユーザーの認可の検証は、UAuth サーバーに登録されているアプリケーションの名前と OAuth クライアントスコープの 2 つのパラメータを明示的に示すことで行えます。

Parameter OAUTH2APPNAME = "OAuthClient";
set isAuthorized = ##class(%SYS.OAuth2.AccessToken).IsAuthorized(
..#OAUTH2APPNAME,
.sessionId,
"scope1",
.accessToken,
.idtoken,
.responseProperties,
.error)

認可がない場合に備え、ユーザー ID をリクエストして、アプリケーションを操作する許可を取得するためのリンクを準備しておきます。 ここでは、OAuth サーバーに登録されているアプリケーションの名前を指定して、OAuth クライアントと要求されるボリューム(スコープ)を入力するだけでなく、ユーザーを返す Web アプリケーションのポイントへのバックリンクも指定する必要があります。

Parameter OAUTH2CLIENTREDIRECTURI = "https://52773b-76230063.labs.learning.intersystems.com/oauthclient/"
set url = ##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
..#OAUTH2APPNAME,
"scope1",
..#OAUTH2CLIENTREDIRECTURI,
.properties,
.isAuthorized,
.sc)

 

IRIS を使用して、ユーザーを IRIS OAuth サーバーに登録しましょう。 たとえば、ユーザーに名前とパスワードを設定するだけで十分です。
受信した参照の下でユーザーを転送すると、サーバーはユーザーを識別する手続きを実行し、Web アプリケーションのアカウントデータによって、操作許可が照会されます。また、%SYS フィールドのグローバル OAuth2.Server.Session で自身に結果を保持します。

  1. 認可されたユーザーのデータを示します。 手続きが正常に完了したら、アクセストークンなどがあります。 それを取得しましょう。

    set valid = ##class(%SYS.OAuth2.Validation).ValidateJWT( .#OAUTH2APPNAME, accessToken, "scope1", .aud, .JWTJsonObject, .securityParameters, .sc )

以下に、完全に動作する OAuth の例のコードを示します。

Class OAuthClient.REST Extends %CSP.REST
{
Parameter OAUTH2APPNAME = "OAuthClient";
Parameter OAUTH2CLIENTREDIRECTURI = "https://52773b-76230063.labs.learning.intersystems.com/oauthclient/";
// to keep sessionId
Parameter UseSession As Integer = 1;
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
    &lt;Routes>
        &lt;Route Method="GET" Url = "/" Call = "Do" />
    &lt;/Routes>
}
ClassMethod Do() As %Status
{
    // Check for accessToken
    set isAuthorized = ##class(%SYS.OAuth2.AccessToken).IsAuthorized(
        ..#OAUTH2APPNAME,
        .sessionId,
        "scope1",
        .accessToken,
        .idtoken,
        .responseProperties,
        .error)
    // to show accessToken
    if isAuthorized {
     set valid = ##class(%SYS.OAuth2.Validation).ValidateJWT(
         ..#OAUTH2APPNAME,
         accessToken,
         "scope1",
         .aud,
         .JWTJsonObject,
         .securityParameters,
         .sc
     )
     &html&lt; Hello!&lt;br> >
         w "You access token = ", JWTJsonObject.%ToJSON()
     &html&lt; &lt;/html> >
     quit $$$OK
    }
    // perform the process of user and client identification and get accessToken
    set url = ##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
        ..#OAUTH2APPNAME,
        "scope1",
        ..#OAUTH2CLIENTREDIRECTURI,
        .properties,
        .isAuthorized,
        .sc)
    if $$$ISERR(sc) {
         w "error handling here"
         quit $$$OK
    }
    // url magic correction: change slashes in the query parameter to its code
    set urlBase = $PIECE(url, "?")
    set urlQuery = $PIECE(url, "?", 2)
    set urlQuery = $REPLACE(urlQuery, "/", "%2F")
    set url = urlBase _ "?" _ urlQuery
    &html&lt;
     &lt;html>
         &lt;h1>Authorization in IRIS via OAuth2&lt;/h1>
         &lt;a href = "#(url)#">Authorization in &lt;b>IRIS&lt;/b>&lt;/a>
     &lt;/html>
    >
    quit $$$OK
}
}

コードの作業コピーは、InterSystems GitHub リポジトリ(https://github.com/intersystems-community/iris-oauth-example)にもあります。

必要に応じて、OAuth サーバーと OAuth クライアントに高度なデバッグメッセージモードを有効にしてください。これらは、%SYS エリアの ISCLOG グローバルに記述されます。

set ^%ISCLOG = 5
set ^%ISCLOG("Category", "OAuth2") = 5
set ^%ISCLOG("Category", "OAuth2Server") = 5

詳細については、「IRIS、OAuth 2.0 と OpenID Connect の使用」ドキュメントをご覧ください。

まとめ

これまで見てきたように、すべての OAuth 機能には簡単にアクセスでき、完全に使用できる状態になっています。 必要に応じて、ハンドラークラスとユーザーインターフェースを独自のものに置き換えることができます。 OAuth サーバーとクライアントの設定は、管理ポータルを使う代わりに、構成ファイルで構成することも可能です。

0
0 417
記事 Toshihiko Minamoto · 7月 28, 2021 7m read

この 3 部構成の記事では、IAM を使って、以前に IRIS にデプロイされた認証されていないサービスに OAuth 2.0 標準に従ったセキュリティを追加する方法を説明します。

パート 1 では、サービスを保護するプロセス全体を理解しやすくするために、IRIS と IAM の基本的な定義と構成を示しながら OAuth 2.0 の背景を説明しました。

パート 2 では、着信リクエストに存在するアクセストークンを検証し、検証が成功した場合にはそのリクエストをバックエンドに転送するように IAM を構成する手順について詳しく説明しました。

連載の最後となるこのパートでは、IAM がアクセストークンを生成(承認サーバーとして機能します)してそれを検証するために必要な構成と、重要な最終考慮事項を説明します。

IAM をお試しになりたい方は、InterSystems 営業担当者にお問い合わせください。

シナリオ 2: 承認サーバーとアクセストークンのバリデーターとしての IAM

このシナリオでは、最初のシナリオとは異なり、「OAuth 2.0 Authentication」というプラグインを使用します。

このリソース所有者パスワード資格情報フローで IAM を承認サーバーとして使用するには、クライアントアプリケーションがユーザー名とパスワードを認証する必要があります。 IAM からアクセストークンを取得するリクエストは、認証が成功した場合にのみ行う必要があります。

プラグインを「SampleIRISService」に追加しましょう。 以下のスクリーンショットからわかるように、このプラグインを構成するために入力が必要なフィールドがいくつかあります。

まず、「SampleIRISService」の ID を[service_id]フィールドに貼り付けて、このプラグインをサービスに適用します。

[config.auth_header_name]フィールドには、承認トークンを運搬するヘッダー名を指定します。 ここでは、デフォルト値の「authorization」のままにします。

「OAuth 2.0 Authentication」プラグインは、認可コードグラント、クライアント資格情報、インプリシットグラント、またはリソース所有者パスワード資格情報グラントの OAuth 2.0 フローをサポートしています。 この記事ではリソース所有者パスワードを使用しているため、[config.enable_password_grant]チェックボックスをオンにします。

[config.provision_key]フィールドには、プロビジョンキーとして使用される任意の文字列を入力します。 この値は、IAM にアクセストークンをリクエストするために使用されます。

ここでは、ほかのすべてのフィールドはデフォルト値のままにしました。 各フィールドの完全なリファレンスは、こちらからアクセスできるプラグインのドキュメントをご覧ください。

最終的に、プラグイン構成は次のようになります。

プラグインが作成されたら、「ClientApp」コンシューマーへの資格情報を作成する必要があります。

作成するには、左メニューの[コンシューマー]に移動して[ClientApp]をクリックします。 次に[資格情報]タブをクリックして[新しい OAuth 2.0 アプリケーション]ボタンをクリックします。

次のページでは、[名前]フィールドにアプリケーションを識別するための任意の名前を入力し、[client_id]フィールドと[client_secret]にクライアント ID とクライアントシークレットを定義し、[redirect_uri]フィールドに承認後にユーザーが送信されるアプリケーションの URL を入力します。 そして、[作成]をクリックします。

これで、リクエストを送信する準備が整いました。

最初に行うのは、IAM からアクセストークンを取得するためのリクエストです。 「OAuth 2.0 Authentication」プラグインは自動的に、作成済みのルートに「/oauth2/token」パスを追加して、エンドポイントを作成します。

注意: HTTPS プロトコルと、TLS/SSL リクエストをリスンする IAM のプロキシポート(デフォルトポートは 8443)を使用していることを確認してください。 これは OAuth 2.0 の要件です。

したがって、この場合、次の URL に POST リクエストを行う必要があります。

https://iamhost:8443/event/oauth2/token

リクエスト本文に、次の JSON を含める必要があります。

{
   "client_id": "clientid",
   "client_secret": "clientsecret",
   "grant_type": "password",
   "provision_key": "provisionkey",
   "authenticated_userid": "1"
}

ご覧のとおり、この JSON には「OAuth 2.0 Authentication」プラグイン作成中に定義した値(「grant_type」や「provision_key」など)とコンシューマーの資格情報の作成中に定義した値(「client_id」や「client_secret」など)が含まれています。

提供されたユーザー名とパスワードが正しく認証された場合には、クライアントアプリケーションによって「authenticated_userid」パラメーターも追加される必要があります。 この値は、認証されたユーザーを一意に識別するために使用されます。

リクエストとそれに対応するレスポンスは次のようになります。

これで、上記のレスポンスの「access_token」値を次の URL への GET リクエストの「ベアラートークン」として含めて、イベントデータを取得するリクエストを行えるようになりました。

https://iamhost:8443/event/1

アクセストークンが期限切れになった場合は、アクセストークンを取得するために使用したのと同じエンドポイントに、わずかに異なる本文を使って POST リクエストを送信し、期限切れのアクセストークンととも受け取ったリフレッシュトークンを使用して、新しいアクセストークンを生成することができます。

{
   "client_id": "clientid",
   "client_secret": "clientsecret",
   "grant_type": "refresh_token",
   "refresh_token": "E50m6Yd9xWy6lybgo3DOvu5ktZTjzkwF"
}

リクエストとそれに対応するレスポンスは次のようになります。

「OAuth 2.0 Authentication」プラグインには、アクセストークンを表示して無効にするという興味深い機能があります。

トークンを一覧表示するには、次に示す IAM の管理 API のエンドポイントに GET リクエストを送信します。

https://iamhost:8444/{workspace_name}/oauth2_tokens

上記の {workspace_name} は使用される IAM ワークスペースの名前です。 RBAC を有効している場合に備え、IAM の管理 API を呼び出すために必要な資格情報を入力してください。

「credential_id」は ClientApp コンシューマー内に作成した OAuth アプリケーションの ID(この場合は SampleApp)で、「service_id」はこのプラグインが適用される「SampleIRISService」の ID であることに注意してください。

トークンを無効にするには、次のエンドポイントに DELETE リクエストを送信します。

https://iamhost:8444/Sample/oauth2_tokens/{token_id}

上記の {token_id} は無効にされるトークンの ID です。

無効化されたトークンを使おうとした場合、この無効なトークンをベアラートークンとして含む GET リクエストを 次の URL に送信すると、トークンが無効であるか期限切れであるというメッセージが表示されます。

https://iamhost:8443/event/1

最終的な考慮事項

この記事では、IRIS にデプロイされている認証されていないサービスに対し、IAM で OAuth 2.0 認証を追加する方法を示しました。 サービスそのものは、IRIS で認証されないままとなることに注意してください。 したがって、IRIS サービスのエンドポイントを IAM レイヤーを介さずに直接呼び出すと、情報は認証なしで表示されます。 そのため、ネットワークレベルでセキュリティルールを設定し、不要なリクエストが IAM レイヤーをバイパスしないようにすることが重要です。

IAM の詳細についてはこちらをご覧ください。

IAM をお試しになりたい方は、InterSystems 営業担当者にお問い合わせください。

0
0 226
記事 Toshihiko Minamoto · 7月 26, 2021 5m read

この 3 部構成の記事では、IAM を使って、以前に IRIS にデプロイされた認証されていないサービスに OAuth 2.0 標準に従ったセキュリティを追加する方法を説明します。

パート 1 では、サービスを保護するプロセス全体を理解しやすくするために、IRIS と IAM の基本的な定義と構成を示しながら OAuth 2.0 の背景を説明しました。

このパートでは、着信リクエストに存在するアクセストークンを検証し、検証が成功した場合にはそのリクエストをバックエンドに転送するように IAM を構成する手順について詳しく説明します。

この連載の最後のパートでは、IAM でアクセストークンを生成し(承認サーバーとして機能します)、それを検証するようにするための構成を説明し、重要な最終考慮事項を示します。

IAM をお試しになりたい方は、InterSystems 営業担当者にお問い合わせください。

シナリオ 1: アクセストークンのバリデーターとして機能する IAM

このシナリオでは、JWT(JSON Web トークン)形式でアクセストークンを生成する外部承認サーバーを使用します。 この JWT はアルゴリズム RS256 と秘密鍵を使用して署名されています。 JWT 署名を検証するには、ほかのグループ(この場合 IAM)に承認サーバーが提供する秘密鍵が必要です。

外部承認サーバーが生成するこの JWT には、本体に、このトークンの有効期限を示すタイムスタンプを含む「exp」と呼ばれるクレームと、承認サーバーのアドレスを含む「iss」と呼ばれる別のクレームも含まれます。

したがって、IAM はリクエストを IRIS に転送する前に、承認サーバーの秘密鍵と JWT 内の「exp」クレームに含まれる有効期限のタイムスタンプを使用して、この JWT 署名を検証する必要があります。

これを IAM で構成するために、まず、IAM の「SampleIRISService」に「JWT」というプラグインを追加しましょう。 追加するには、IAM のサービスページに移動して「SampleIRISService」の ID をコピーします。これは後で使用します。

コピーしたら、プラグインに移動して[新規プラグイン]ボタンをクリックし、「JWT」プラグインを見つけて[有効化]をクリックします。

次のページで、[service_id]フィールドに「SampleIRISService」の ID を貼り付け、[config.claims_to_verify]パラメーターの「exp」ボックスを選択します。


[config.key_claim_name]パラメーターの値が「iss」であることに注意してください。 これは後で使用します。

次に、[作成]ボタンをクリックします。

クリックしたら、左メニューの「コンシューマー」セクションに移動し、前に作成した「ClientApp」をクリックします。 [資格情報]タブに移動し、[新しい JWT 資格情報]ボタンをクリックします。

次のページで、JWT の署名に使用されるアルゴリズム(この場合 RS256)を選択肢、[rsa_public_key]フィールドに公開鍵(PEM 形式で承認サーバーから提供された公開鍵)を貼り付けます。

[鍵]フィールドには、JWT プラグインを追加したときに[config.key_claim_name]フィールドに入力した JWT クレームのコンテンツを挿入する必要があります。 したがって、この場合は、JWT の iss クレームのコンテンツを挿入する必要があります。私の場合、このコンテンツは承認サーバーのアドレスです。

挿入したら、[作成]ボタンをクリックします。

ヒント: デバッグ用に、JWT をデコードするオンラインツールがあります。それに公開鍵を貼り付けると、クレームとその値を確認して、署名を検証することができます。 このオンラインツールは https://jwt.io/#debugger にあります。

JWT プラグインが追加されたため、認証無しでリクエストを送信することはできなくなりました。 以下に示すように、単純な GET リクエストを認証なしで次の URL に送信する場合、

http://iamhost:8000/event/1

「401 Unauthorized」ステータスコードで不正なメッセージが返されます。

IRIS から結果を取得するには、リクエストに JWT を追加する必要があります。

したがって、最初に承認サーバーに JWT をリクエストする必要があります。 ここで使用しているカスタム承認サーバーは、POST リクエストが、ユーザーやクライアント情報を含むキー値ペアとともに次の URL に送信された場合に JWT を返します。

https://authorizationserver:5001/auth

このリクエストとそのレスポンスは次のようになります。

次に、レスポンスから取得した JWT を承認ヘッダーの下にベアラートークンとして追加し、以前に使用したのと同じ URL に GET リクエストを送信することができます。

http://iamhost:8000/event/1

または、クエリ文字列パラメーターとして追加することも可能です。クエリ文字列のキーは、JWT プラグインを追加したときに[config.uri_param_names]フィールドに指定された値(この場合は「jwt」)です。

最後に、[config.cookie_name]フィールドに名前が入力されている場合は、JWT を cookie としてリクエストに含めるオプションもあります。

IAM でアクセストークンを生成して検証するために必要な構成と重要な最終考慮事項を理解するには、この連載のパート 3 であり最後となる記事をご覧ください。

0
0 196
記事 Toshihiko Minamoto · 7月 19, 2021 5m read

はじめに

近年、オープン認証フレームワーク(OAuth)を使って、あらゆる種類のサービスから信頼性のある方法で安全かつ効率的にリソースにアクセスするアプリケーションが増えています。 InterSystems IRIS はすでに OAuth 2.0 フレームワークに対応しており、事実コミュニティには、OAuth 2.0 と InterSystems IRIS に関する素晴らしい記事が掲載されています。

しかし、API 管理ツールの出現により、一部の組織はそのツールを単一の認証ポイントとして使用し、不正なリクエストが下流のサービスに到達するのを防ぎ、サービスそのものから承認/認証の複雑さを取り除いています。

ご存知かもしれませんが、InterSystems は、IRIS Enterprise ライセンス(IRIS Community Edition ではありません)で利用できる InterSystems API Management(IAM)という API 管理ツールを公開しています。 こちらには、InterSystems API Management を紹介する素晴らしい別のコミュニティ記事が掲載されています。

 これは、IAM を使って、以前に IRIS にデプロイされた認証されていないサービスに OAuth 2.0 標準に従ったセキュリティを追加する方法を説明した 3 部構成記事の最初の記事です。

サービスを保護するプロセス全体を理解しやすくするために、最初の記事では、IRIS と IAM の基本的な定義と構成を示しながら OAuth 2.0 の背景を説明します。

この連載記事のパート 2 以降では、IAM によってサービスを保護する上で考えられる 2 つのシナリオを説明します。 最初のシナリオでは、IAM は着信リクエストに存在するアクセストークンを検証し、検証が成功した場合にのみリクエストを 転送します。 2 番目のシナリオでは、IAM がアクセストークンを生成し(承認サーバーとして機能します)、それを検証します。

従って、パート 2 では、シナリオ 1 を構成するために必要な手順を詳しく説明し、パート 3 ではシナリオ 2 の構成を説明した上で、最終的な考慮事項を示します。

IAM をお試しになりたい方は、InterSystems 営業担当者にお問い合わせください。

OAuth 2.0 の背景

すべての OAuth 2.0 承認フローには基本的に以下の 4 つのグループが関わっています。

  1. ユーザー
  2. クライアント
  3. 承認サーバー
  4. リソース所有者

分かりやすくするために、この記事では「リソース所有者パスワード資格情報」OAuth フローを使用しますが、IAM ではあらゆる OAuth フローを使用できます。 また、この記事では範囲を指定しません。

注意: クライアントアプリはユーザー資格情報を直接処理するため、クライアントアプリの信頼性が非常に高い場合にのみリソース所有者パスワード資格情報フローを使用することをお勧めします。 ほとんどの場合、クライアントはファーストパーティアプリである必要があります。

以下は、一般的なリソース所有者パスワード資格情報フローの手順です。

  1. ユーザーはクライアントアプリに資格情報(ユーザー名とパスワードなど)を入力します。
  2. クライアントアプリは承認サーバーにユーザー資格情報と独自の ID(クライアント ID とクライアントシークレットなど)を送信します。 承認サーバーはユーザー資格情報とクライアント ID を検証し、アクセストークンを返します。
  3. クライアントはトークンを使用して、リソースサーバーにあるリソースにアクセスします。
  4. リソースサーバーは受け取ったアクセストークンを検証してから、クライアントに情報を返します。

これを踏まえ、IAM を使用して OAuth 2.0 を処理できるシナリオが 2 つあります。

  1. IAM はバリデーターをして機能し、クライアントアプリが提供するアクセストークンを検証し、アクセストークンが有効である場合にのみリソースサーバーにリクエストを転送します。この場合、アクセストークンはサードパーティの承認サーバーによって生成されます。
  2. IAM は承認サーバーとしてクライアントにアクセストークンを提供し、アクセストークンのバリデーターとしてアクセストークンを検証してから、リソースサーバーにリクエストをリダイレクトします。

IRIS と IAM の定義

この記事では、「/SampleService」という IRIS Web アプリケーションを使用します。 以下のスクリーンショットからわかるように、これは IRIS にデプロイされた認証されていない REST サービスです。

さらに、以下のスクリーンショットのとおり、IAM 側では 1 つのルートを含む「SampleIRISService」というサービスが構成されています。

また、IAM では、IAM で API を呼び出しているユーザーを識別するために、最初に資格情報の無い「CliantApp」というコンシューマーが構成されています。

上記の構成により、IAM は次の URL に送信されるすべての GET リクエストを IRIS にプロキシしています。

http://iamhost:8000/event

この時点では、認証は使用されていません。 したがって、認証無しで単純な GET リクエストを次の URL に送信する場合、

http://iamhost:8000/event/1

必要なレスポンスを得られます。

この記事では、「PostMan」というアプリを使用してリクエストを送信し、レスポンスを確認します。 以下の PostMan のスクリーンショットでは、単純な GET リクエストとそのレスポンスを確認できます。

着信リクエストに存在するアクセストークンを検証するように IAM を構成する方法を理解するには、この連載のパート 2 をお読みください。

0
0 197
記事 Shintaro Kaminaka · 4月 15, 2021 9m read

開発者の皆さん、こんにちは。 以前の記事でIRIS for Health上でFHIRリポジトリを構築し、OAuth2認証を構成する方法をご紹介しました。

この代行認証編では、IRIS for HealthのFHIRリポジトリに組み込まれた認証機能ではなく、IRISの代行認証機能+ZAUTHENTICATEルーチンを使用して認証を行う方法をご紹介します。

前回記事でご紹介したように、標準のFHIRリポジトリの認証機構では、アクセストークンの発行先を追加するためのAudienceの指定(aud=https://~) や、アクセストークンだけではなくベーシック認証の情報を送付するなどの対応が必要でした。

スクラッチでFHIRクライアントを開発するのではなく、既成の製品やアプリケーションからアクセスする場合、上記のような処理の実装ができないことがあるかもしれません。 そのような場合には、この代行認証+ZAUTHENTICATEルーチンを使用して、カスタマイズした認証の仕組みを構築することができます。

この記事に含まれる情報のドキュメントについて

この記事で記載されている情報はIRIS for Healthのドキュメントにも含まれている内容をわかりやすく再構成したものです。

RESTサービスの保護:RESTアプリケーションおよびOAuth2.0

OAuth 2.0 クライアントとしての InterSystems IRIS Web アプリケーションの使用法


代行認証を有効にする

まず使用しているIRIS環境で「代行認証」機能を有効にし、アクセスするFHIRリポジトリの「Webアプリケーション設定」で「代行認証」機能を使える用に構成します。


認証/ウェブセッションオプション画面

まずシステムとして「代行認証」が使用できるように構成します。

管理ポータルの システム管理→セキュリティ→システム・セキュリティ→認証/ウェブセッションオプション と進み、「代行認証を許可」をチェックします。 「代行認証によるOS認証を許可」ではありませんのでご注意ください。

image


%Service_WebGatewayサービス 画面

次に、CSPゲートウェイを経由したWebのアクセスに対して、「代行認証」が有効になるよう構成します。

管理ポータルの システム管理→セキュリティ→サービス と進み、「%Service_WebGateway」をクリックして、許可された認証方法の「代行」にチェックがついていることを確認します。もしチェックされていなければ、チェックして保存を実行してください。

image


FHIRリポジトリの ウェブアプリケーションの編集 画面

最後に、アクセスするFHIRリポジトリの ウェブ・アプリケーションの編集画面で「代行認証」を有効にします。

管理ポータルの システム管理→セキュリティ→アプリケーション→ウェブ・アプリケーション と進み、該当のFHIRリポジトリアプリケーションを選択します。 特に変更をしていなければ、/csp/healthshare/<namespace>/fhir/r4 となっています。

この画面で、セキュリティの設定:許可された認証方法の「代行」をチェックして保存します。

image

これで、「代行認証」を利用する準備はOKです。次は、実際に代行認証のためのロジックが記載されたZAUTHENTICATEルーチンを用意します。


ZAUTHETICATEルーチンの入手とインポート

ZAUTHENTICATEルーチンのサンプルはInterSystemsのGitHubで公開されています。

GitHub:Samples-Security

この記事ではここで紹介されているREST.ZAUTHENTICATE.macルーチンを利用します。 GitHubのREADMEに記載されているこのルーチンの説明をここにも転載します。

  • REST.ZAUTHENTICATE.mac is another sample routine that demonstrates how to authenticate a REST application using OAuth 2.0. To use this sample:
    1. Configure the resource server containing the REST application as an OAuth 2.0 resource server.
    2. Copy this routine to the %SYS namespace as ZAUTHENTICATE.mac.
    3. Modify value of applicationName in ZAUTHENTICATE.mac.
    4. Allow delegated authentication for %Service.CSP.
    5. Make sure that the web application for the REST application is using delegated authentication.

この記事では、先に手順の4.,5.を済ませているので、ルーチンのインポートを実施しましょう。 (上記READMEでは、%Service.CSPと記載されていますが、現在は%Service_WebGatewayになっています。)

GitHubからルーチンをダウンロードしてインポートするか、あるいは、このリンクから直接ルーチンを表示し、中身をStudioやVS Codeのエディタを使ってコピーしてZAUTHENTICATEルーチンをつくることもできます。%SYSネームスペースに作成します。

(注意:2021/4/16時点ではこのルーチンをスタジオからインポートするとエラーが発生してしまいます。お手数ですが、ファイルの中身をコピーしてZAUTHENTICATEルーチンを作成する方法で回避してください。)

ZAUTHENTICATEルーチンを作成したら、applicationNameを変更します。これは前回の記事で記載したOAuth2クライアントアプリケーションの クライアント構成 画面で作成した「アプリケーション名」を指定します。

image

ここでは前回の記事にならい「FHIRResource」としています。コードの一部を紹介します。

// Usually you will need to modify at least the roles to be assigned.
set roles="%DB_DEFAULT,%Operator"

$$$SysLog(3,"OAuth2","[ZAUTHENTICATE]","ServiceName="_ServiceName_", Username="_Username_", roles="_roles)

// MUST BE MODIFIED FOR EACH INSTANCE WHICH USES OAuth 2.0 for REST.
// The application name used to define the authorization server to this resource server.
set applicationName="FHIRResource"

set loginSuccessful=0
set errorText=""

コードを変更したらコンパイルを実行します。

このZAUTHENTICATEルーチンで重要なのは以下のコード部分です。 GetAccessTokenFromRequestメソッドを使用してHTTPリクエストからアクセストークンを取り出し、ValidateJWTメソッドを使用してValidationを実施し正しいアクセストークンであることを確認しています。

// This ZAUTHENTICATE routine will support OAuth 2.0 based
// delegated authentication for subclasses of %CSP.REST.
set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)

// Check if authorized.
// if the access token is not a JWT, we would need to validate the access token
// using another means such as the introspection or userinfo endpoint.
if $$$ISOK(sc) {
	set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(applicationName,accessToken,,,.jsonObject,,.sc)
}

POSTMANからのテスト

それでは前回同様、RESTクライアントツールのPOSTMANからテストしてみましょう。 前回同様、まずはアクセストークンを取得します。

前回とは異なり、Auth URLにaudパラメータを追加する必要はありません。トークンを取得できたら、「Use Token」ボタンをクリックし、そのトークンを使用できるようにします。

次は、FHIRリポジトリへのアクセスです。今回は前回と異なり、ベーシック認証と組み合わせる必要はありませんので、そのままFHIRリポジトリにアクセスするRESTのURLのみを入力し、実行します。

FHIRリソースが取得できたら成功です。

2020.4以降の対応

IRIS for Health 2020.4ではこちらの記事に掲載したように、FHIRリポジトリ上でアクセストークンのスコープ情報がチェックされるようになりました。 このため、セキュリティ用件にも依存しますが、ZAUTHENTICATEルーチンで必ずしもアクセストークンのValidationチェックを行う必要はありません。 これまでこのシリーズで紹介してきましたように、IRIS for HealthがOAuth2認可サーバの役割も兼ねている場合、2020.4上で動かす最も単純な方法は、ZAUTHENTICATEルーチンのGetCredentialsラベルで、アクセストークンを取得する際にも指定したIRISパスワードユーザを返すようにすることです。

例:アクセストークンを取得した際のユーザと image

同じユーザを返すようにする。(このdaikoユーザには%All権限を与えています)

GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public {
	if ServiceName="%Service_WebGateway" {
		// Supply user name and password for authentication via a subclass of %CSP.REST
		set Username="daiko"
		set Password="xxxxx"
	}
	quit $$$OK
}

こちらの代行認証に関するドキュメントに記載があるように、GetCredentialsラベルで実在するIRISパスワードユーザが返された場合はそのユーザに認証が行われるため、ZAUTHENTICATEルーチンで実行されていたアクセストークンのValidationチェックのロジックは実施されなくなります。 ただし、アクセストークンの検証はその後FHIRリポジトリ上で実施されるため不正なアクセストークンでアクセスしたりすることはできません。 なお、2020.4では、スコープのチェックやAudience情報のチェックも行われるため、このバージョンの代行認証では aud=https://~ の情報の追加や適切なスコープ指定も必要になります。

テストとしてはこの方法で動作を確認することができると思いますが、もちろん実際のアプリケーションで実装する場合は、より複雑な状況を考慮に入れる必要があるでしょう。例えば、アクセスするユーザごとに異なるIRISパスワードユーザをもつケースもあれば、そもそもアクセストークンの発行元であOAuth2認可サーバが、IRISではなく他のサービスである可能性もあります。後者のようなケースでは、このZAUTHENTICATEルーチン上で代行ユーザを作成する必要があり、さらにそのユーザ名はアクセストークン内のユーザ情報(sub)と一致する必要があります。

残念ながらこの記事でそれらの状況をすべてカバーすることはできませんが、この記事に記載されている情報がIRIS for HealthのFHIRリポジトリを活用したセキュアなアプリケーション構築の一助となれば幸いです。

0
0 474
記事 Shintaro Kaminaka · 4月 15, 2021 7m read

開発者の皆さん、こんにちは。

以前の記事でIRIS for Health上でFHIRリポジトリを構築し、OAuth2認証を構成する方法をご紹介しました。 IRIS for Health 2020.4がリリースされ、FHIRリポジトリのアクセストークンをチェックする機能が追加になりました。

ドキュメントはAccess Token Scopesです。

この記事ではドキュメントの記載も抜粋しながらこの機能を紹介していきます。

Basic Processing

The access token that accompanies a request must include at least one patient clinical scope or user clinical scope, or else the request is rejected with an HTTP 403 error. If an access token contains both a patient clinical scope and a user clinical scope, the FHIR server enforces the patient clinical scope while ignoring the user clinical scope.

(訳)リクエストに添付されるアクセストークンには、患者のクリニカル・スコープまたはユーザーのクリニカル・スコープが少なくとも1つ含まれていなければならず、そうでなければリクエストはHTTP 403エラーで拒否されます。アクセストークンに患者のクリニカル・スコープとユーザーのクリニカル・スコープの両方が含まれている場合、FHIRサーバーは患者のクリニカル・スコープを強制し、ユーザーのクリニカル・スコープを無視します。

この機能が追加になったことで、以前の記事でご紹介していたような、"scope1"のようなスコープ指定では403エラーとなり、FHIRリポジトリから応答を受け取ることはできなくなりました。(前回の構成が残っている方はぜひ2020.4にアップグレードして403エラーになることを試してみてください。)

この記事では、正しいスコープを指定して、FHIRリポジトリにアクセスする方法をご紹介します。 ベースとなるOAuth2サーバ構成やクライアント構成に関しては以前と変わりませんので、以前の記事を参考にしてください。


SMART on FHIR : Scopes and Launch Context

今回のIRISの実装は、SMART on FHIRプロジェクトのスコープ定義をベースにしています。

SMART App Launch: Scopes and Launch Context

このサイトのQuick Startに記載されている以下のスコープをベースにこの記事の検証を進めてみたいと思います。 詳細についてはSMART on FHIRの上記解説サイトをご覧ください。

ScopeGrant
patient/*.read現在の患者のあらゆるリソースを読むことができる許可
user/*.*現在のユーザーがアクセスできるすべてのリソースの読み取りと書き込みの許可


OAuth 2.0 認可サーバ構成の変更

まず、OAuth2認可サーバ構成で、サポートしていないスコープが許可されるように構成します。 OAuth2認可サーバの「スコープ」タブで、「サポートしていないスコープを許可」にチェックを入れます。

image


スコープを指定してPatientリソースを登録する

それでまず、Patientリソースを登録してみましょう。 Create Interactionを行う場合には、user clinical scope が必要になります。以下はIRIS for Healthのドキュメントからの抜粋です。

Create Interaction

Requests to create a new Patient resource must include a user clinical scope that gives write permissions (user/Patient.write or user/Patient.*). You cannot perform a create interaction for a Patient resource with a patient clinical scope; patient clinical scopes must include a patient context value, and the create interaction cannot include a resource id.

(訳)新規にPatientリソースを作成する要求には、書き込み権限(user/Patient.writeまたはuser/Patient.*)を与えるユーザークリニカルスコープを含める必要があります。患者臨床スコープには患者コンテキスト値を含める必要があり、作成インタラクションにリソースIDを含めることはできません

先ほど、でてきたスコープ「user/*.*」を追加して、アクセストークンを取得し、Patientリソースを登録してみましょう。

以下の図はRESTツールPOSTMAN上で、スコープを指定している画面です。

image

任意のスコープを許可する設定が正しく動作していれば以下のような確認画面が表示されます。

image

「許可」をクリックして、アクセストークンを取得します。このアクセストークンを使ってPatientリソースを登録してみましょう。 まず登録するPatientリソースを用意してください。FHIR公式ページから取得しても良いですし、過去のコミュニティ記事から取得しても良いです。

POSTMANを使う場合、Bodyタブに登録したいリソースを貼り付けます。

image

前回記事同様、アクセストークン+ベーシック認証を行う必要があるので、取得したアクセストークンを access_toke=eyXXX としてパラメータに追加し、Authorizationタブではベーシック認証を選択してください。

image

メソッドがPOSTであること、リクエストURLが正しくPatientを指定していることなども確認出来たら、登録を実行してみてください。Status 201 Created が返ってきたら登録は成功です。

登録が成功したら、続いてGETも試してみてください。 このスコープは読み取りの権限もありますから取得できるはずです。

リソースが取得できたら、今登録したPatientリソースのリソースIDも確認してください。以下の例では 1 になります。

image


スコープを指定してPatientリソースを取得する

では、Patientリソースが登録できたので、今度は異なるスコープを指定して先ほどのPatientリソースを取得してみましょう。

患者に紐づく情報を取得する場合、patient clinical scope が必要になります。

Patient Clinical Scope / Patient Context Value

If an access token includes a patient clinical scope, it must also include a patient context value (also known as “launch context”) that is the id of a Patient resource. This patient context value provides access to the specified Patient and its related resources.

(訳)アクセストークンに患者の臨床範囲が含まれる場合、Patient リソースの ID である患者コンテキスト値(「起動コンテキスト」とも呼ばれる)も含まれなければならない。この患者コンテキスト値は、指定された患者及びその関連リソースへのアクセスを提供する。

今回はスコープに「patient/*.read」を追加します。もう一点どの患者情報にアクセスできるのか判断するために、PatientリソースのリソースID情報を含むPatient Context Valueをスコープとして渡します。具体的には、「launch/patient/1」(リソースID=1の場合)もスコープとして指定します。

POSTMANでは以下のように指定します。スコープの間は半角スペースで区切ります。

image

アクセストークンを取得できたら、先ほどと同じ手順でリクエストを投げてみましょう。 ただし、今回取得したアクセストークンで取得できるFHIRリソースはリソースID=1のPatientリソースに紐づく情報だけであることにご注意ください。ということは、取得するためにRESTのパスは Patient/1 となります。

例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient/1?access_token=eyXXX

無事に先ほどのPatientリソースが取得できたらアクセス成功です! 次は、このトークンが本当にリソースID=1だけに制限されているかも確認してみましょう。他のリソースIDを指定するか、Patientリソース全体を取得するようなリクエストを投げてみてください(ただし存在しないPatientのリソースIDを指定するとそのリソースは存在しませんという別のエラーになります。)

例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient/2?access_token=eyXXX 例:https://<server>/csp/healthshare/fhirserver/fhir/r4/Patient?access_token=eyXXX

Status 403エラーになりましたか?エラーが返れば確認は成功です。


まとめ

どのようなFHIRリソースをFHIRリポジトリに格納しているのか?また、どのようなアプリケーションを構築して、どのようにユーザにアクセス制限を設定したいのか?等により、利用すべきスコープの使い方は異なってきます。

安全なFHIRアプリケーションを構築するために、この新しいアクセストークンスコープ機能(とあとこの記事が)活用されると幸いです。

0
0 510
記事 Shintaro Kaminaka · 9月 11, 2020 8m read

開発者の皆さん、こんにちは。

今回の記事では前回の記事に引き続き、IRIS for Health上で、FHIRリポジトリ+OAuth2認可サーバ/リソースサーバを構成する方法をご案内します。

(注意:2020.4以降のバージョンではこの記事に記載されているスコープ指定では正しくリソースが取得できません。詳細はこちらの記事をご覧ください。)

パート1では、事前準備と、OAuth2認可サーバを構成し、アクセストークンを取得するとこまでをご紹介しました。
このパート2では、FHIRリポジトリの構築方法と、OAuth2クライアント/リソースサーバの構成方法をご紹介していきます。

今日構成する、FHIRリポジトリおよび、OAuth2クライアント/リソースサーバの構成は、前回パート1で構成したOAuth2認可サーバのIRISインスタンスと分けることもできますし、同じインスタンスに同居させることもできます。
この記事の中では前回と同じインスタンス上に構成していきます。

FHIRリポジトリの構築とOAuth Client Nameの指定

FHIRリポジトリの構築方法は、過去の記事「Azure上でIRIS for Healthをデプロイし、FHIR リポジトリを構築する方法」で紹介しています。

5
0 794
記事 Shintaro Kaminaka · 9月 4, 2020 13m read

開発者の皆さん、こんにちは。

今回の記事ではFHIRと組み合わせて使用されるケースが増えてきている、権限の認可(Authorization)を行うためのOAuth2について取り上げます。

まずこのパート1では、IRIS for HealthおよびApacheのDockerコンテナの起動と、IRIS for Health上で、OAuth2認可サーバ機能を構成し、REST開発ツールPostmanからアクセスし、アクセストークンを取得する方法について解説します。
さらにパート2以降では、IRIS for HealthにFHIRリポジトリ機能を追加し、OAuth2リソースサーバ構成を追加して、Postmanからアクセストークンを使用したFHIRリクエストの実行方法まで解説します。

InterSystems製品のOAuth2機能の解説については、すでにいくつかの記事が開発者コミュニティ上に公開されていますが、今回は改めて最新バージョンでの構成方法を解説したいと思います。
InterSystems IRIS Open Authorization Framework(OAuth 2.0)の実装 - パート1

1
0 1436
記事 Shintaro Kaminaka · 8月 26, 2020 23m read

作成者:Daniel Kutac(InterSystems セールスエンジニア)

パート 3. 付録

InterSystems IRIS OAUTH クラスの説明

この連載の前のパートでは、InterSystems IRIS を OAUTH クライアントおよび認可/認証サーバー(OpenID Connect を使用)として機能するように構成する方法について学びました。 この連載の最後のパートでは、InterSystems IRIS OAuth 2.0 フレームワークを実装するクラスについて説明します。 また、一部の API クラスのメソッドの使用例についても説明します。

OAuth 2.0 を実装する API クラスは、目的に応じて 3 種類のグループに分けることができます。 すべてのクラスは %SYS ネームスペースで実装されています。 これらの一部は(% package 経由で)公開されていますが、一部は非公開になっており、開発者が直接呼び出すことはできません。

内部クラス

これらのクラスは OAuth2 パッケージに属しています。

次の表に、対象となるクラスの一部を掲載しています(クラスの完全なリストについては、CachéあるいはIRIS インスタンスのオンラインクラスリファレンスを参照してください)。 以下の表に掲載されているものを除き、これらのクラスをアプリケーション開発者が直接使用することはできません。

  <td>
    説明
  </td>
</tr>

<tr style="height:0px">
  <td>
    OAuth2.AccessToken
  </td>
  
  <td>
    Persistent(永続クラス)

OAuth2.AccessToken は、OAuth 2.0 アクセストークンとその関連情報を格納します。 これは、アクセストークンの OAUTH クライアントコピーです。OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

<tr style="height:0px">
  <td>
    OAuth2.Client
  </td>
  
  <td>
    Persistent(永続クラス)

OAuth2.Application クラスは OAuth2 クライアントを記述し、RFC 6749 に基づいてアプリケーションを認可するために使用する認可サーバーを参照します。 クライアントシステムは、さまざまなアプリケーションで複数の認可サーバーと共に使用できます。

<tr style="height:0px">
  <td>
    OAuth2.Response
  </td>
  
  <td>
    CSPページ

これは、InterSystems IRIS OAuth 2.0 クライアントコードから使用される OAuth 2.0 認可サーバーからの応答用のランディングページです。 応答はここで処理され、最終的なターゲットにリダイレクトされます。

<tr style="height:0px">
  <td>
    OAuth2.ServerDefinition
  </td>
  
  <td>
    Persistent(永続クラス)

OAUTH クライアント(この InterSystems IRIS インスタンス)が使用する認可サーバーの情報が格納されています。 認可サーバーの定義ごとに複数のクライアント構成を定義できます。

<tr style="height:0px">
  <td>
    OAuth2.Server.AccessToken
  </td>
  
  <td>
    Persistent(永続クラス)

アクセストークンは OAUTH サーバーの OAuth2.Server.AccessToken によって管理されます。 このクラスには、アクセストークンと関連プロパティが格納されます。 このクラスは、認可サーバーのさまざまな要素間の通信手段でもあります。

<tr style="height:0px">
  <td>
    OAuth2.Server.Auth
  </td>
  
  <td>
    CSP ページ

認可サーバーは、RFC 6749 で指定されている認可コードおよびインプリシットグラントタイプの認可制御フローをサポートします。 OAuth2.Server.Auth クラスは認可エンドポイントとして機能し、RFC 6749 に従ってフローを制御する %CSP.Page のサブクラスです。

<tr style="height:0px">
  <td>
    OAuth2.Server.Client
  </td>
  
  <td>
    Persistent(永続クラス)

OAuth2.Server.Configuration は、この認可サーバーに登録したクライアントを記述する永続クラスです。

<tr style="height:0px">
  <td>
    OAuth2.Server.Configuration
  </td>
  
  <td>
    Persistent(永続クラス)

認可サーバーの構成が格納されます。 すべての構成クラスには、ユーザーが構成の詳細を入力するためのシステム管理ポータルページがそれぞれ存在します。

クラス名

OAuth2.Client、OAuth2.ServerDefinition、OAuth2.Server.Client、OAuth2.Configuration の各オブジェクトは UI を使用せずに開き、変更し、作成または変更した構成を保存できます。 これらのクラスを使用し、構成をプログラムで操作できます。

サーバーカスタマイズ用クラス

これらのクラスは %OAuth2 パッケージに属しています。 このパッケージには、一連の内部クラス(ユーティリティ)が含まれています。ここでは、開発者が使用できるクラスについてのみ説明します。 これらのクラスは、OAuth 2.0 Server Configuration(サーバー構成)ページで参照されます。

  <td>
    CSPページ

%OAuth2.Server.Authenticate はデフォルトの Authenticate クラスだけでなく、ユーザーが作成したすべての Authenticate クラスのサブクラスとして機能します。 Authenticate クラスは、ユーザーを認証するために OAuth2.Server.Auth の認可エンドポイントによって使用されます。 このクラスを使用すると、認証プロセスをカスタマイズできます。次のメソッドを OAuth2.Server のデフォルトをオーバーライドするために実装できます。 DirectLogin– ログインページを表示しない場合にのみ使用します。 DisplayLogin – 認可サーバーのログインフォームを実装します。 DisplayPermissions – リクエストされたスコープのリストを使用してフォームを実装します。CSS を変更することで、外観や操作性をさらにカスタマイズできます。 CSS スタイルは DrawStyle メソッドで定義されます。loginForm は DisplayLogin フォーム用です。permissionForm は DisplayPermissions フォーム用です。

<tr style="height:0px">
  <td>
    %OAuth2.Server.Validate
  </td>
  
  <td>
    CSP ページ

これは、サーバーに含まれているデフォルトの Validate User Class(ユーザー検証クラス)です。 デフォルトのクラスは、認証サーバーが配置されている Cache またはIRISインスタンスのユーザーデータベースを使用してユーザーを検証します。 サポートされるプロパティは、issuer(Issuer)、role、sub(Username)です。Validate User Class は Authorization Server Configuration(認可サーバーの構成)で指定されます。 ユーザー名とパスワードの組み合わせを検証し、ユーザーに関連付けられたプロパティ一式を返す ValidateUser メソッドを含める必要があります。

<tr style="height:0px">
  <td>
    %OAuth2.Server.Generate
  </td>
  
  <td>
    Registered Object(登録オブジェクト)

%OAuth2.Server.Generate は、サーバーに含まれているデフォルトのGenerate Token Class(トークン生成クラス)です。 デフォルトのクラスは、ランダムな文字列を opaque アクセストークンとして生成します。Generate Token Class は、Authorization Server Configuration で指定されます。 ValidateUser メソッドによって返されるプロパティの配列に基づいてアクセストークンを生成するために使用される GenerateAccessToken メソッドを含める必要があります。

<tr style="height:0px">
  <td>
    %OAuth2.Server.JWT
  </td>
  
  <td>
    Registered Object(登録オブジェクト)

%OAuth2.Server.JWT は、サーバーに含まれている JSON Web トークンを作成する Generate Token Class です。 Generate Token Class は、Authorization Server Configuration で指定されます。 ValidateUser メソッドによって返されるプロパティの配列に基づいてアクセストークンを生成するために使用される GenerateAccessToken メソッドを含める必要があります。

<tr style="height:0px">
  <td>
    %OAuth2.Utils
  </td>
  
  <td>
    Registered Object(登録オブジェクト)

このクラスは、さまざまなエンティティのログの記録を実装します。 カスタマイズの章のサンプルコードに、可能な使用法を示しています。

%OAuth2.Server.Authenticate

次の画像に、OAuth 2.0 認可サーバー構成の対応するセクションを示します。

OpenID Connect を JWT 形式の識別情報トークン(id_token)と共に使用する場合は、構成内のデフォルトの Generate Token Class である %OAuth2.Server.Generate%OAuth2.Server.JWT に置換してください。それ以外の場合は、デフォルトの Generate クラスのままにしてください。

カスタマイズオプションについては、後で別の章で詳しく説明します。

公開 API クラス

公開 API クラスは、アプリケーション開発者が Web アプリケーションのメッセージフローに正しい値を渡し、アクセストークンの検証やイントロスペクションなどを実行するために使用されます。

これらのクラスは %SYS.OAuth2 パッケージで実装されています。 次の表に、実装されているクラスの一部を掲載しています。

  <td>
    Registered Object(登録オブジェクト)

%SYS.OAuth2.AccessToken クラスは、リソースサーバーへの認証にアクセストークンを使用できるようにするクライアント操作を定義します。基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

<tr style="height:0px">
  <td>
    %SYS.OAuth2.Authorization
  </td>
  
  <td>
    Registered Object(登録オブジェクト)

%SYS.OAuth2.Authorization クラスには、アクセストークンを取得してクライアントを認可するために使用される操作が含まれています。基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。このクラスは CACHELIB にあるため、どこでも使用できることに注意してください。 ただし、トークンストレージは CACHESYS にあるため、ほとんどのコードでは直接使用できません。

<tr style="height:0px">
  <td>
    %SYS.OAuth2.Validation
  </td>
  
  <td>
    Registered Object(登録オブジェクト)

%SYS.OAuth2.Validation クラスは、アクセストークンの検証(または無効化)に使用されるメソッドを定義します。 基本となるトークンは、CACHESYS データベースの OAuth2.AccessToken に格納されます。 OAuth2.AccessToken は、SessionId と ApplicationName の組み合わせによってインデックス化されます。 したがって、SessionId/ApplicationName ごとに 1 つのスコープのみをリクエストできます。 2 回目のリクエストが別のスコープで行われ、アクセストークンがまだ付与されている場合は、新しいリクエストのスコープが予期されるスコープになります。

%SYS.OAuth2.AccessToken

このグループのいくつかのメソッドとクラスを詳しく見てみましょう。

アクセストークンを使用するすべてのクライアントアプリケーションクラスは、その有効性をチェックする必要があります。 このチェックは、OnPage メソッド(または ZEN か ZENMojo ページの対応するメソッド)のどこかで実行されます。

こちらがそのコードスニペットです。

 // OAuth2 サーバーからのアクセストークンがあるかどうかをチェックします。
 set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1,
     scope2",.accessToken,.idtoken,.responseProperties,.error)

 // アクセストークンがあるかどうかをさらにチェックします。
 // 以下は実行可能なすべてのテストであり、あらゆるケースで必要なわけではありません。
 // 各テストで返される JSON オブジェクトが単に表示されています。
 if isAuthorized {
    // 何らかの処理を実行します – リソースサーバーの API を呼び出して目的のデータを取得します。
 }

リソースサーバーの API を呼び出すたびに、アクセストークンを提供する必要があります。 この処理は、%SYS.OAuth2.AccessToken メソッドの AddAccessToken メソッドによって実行されます。こちらがそのコードスニペットです。

 set httpRequest=##class(%Net.HttpRequest).%New()
  // AddAccessToken は現在のアクセストークンをリクエストに追加します。
  set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
    httpRequest,,
    ..#SSLCONFIG,
    ..#OAUTH2APPNAME)
 if $$$ISOK(sc) {
    set sc=httpRequest.Get(.. Service API url …)
 }

この連載の前のパートで提供したサンプルコードでは、最初のアプリケーションページ(Cache1N)の OnPreHTTP メソッドでこのコードを確認することができました。 このコードは、アプリケーションの最初のページでアクセストークンチェックを実行するのに最適な場所です。

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
 set scope="openid profile scope1 scope2"
    #dim %response as %CSP.Response
 if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,
    scope,.accessToken,.idtoken,.responseProperties,.error) {
      set %response.ServerSideRedirect="Web.OAUTH2.Cache2N.cls"
 }
 quit 1
}

上記のコードにある SYS.OAuth2.AccessToken クラスの IsAuthorized メソッドは有効なアクセストークンが存在するかどうかをチェックし、存在しない場合に認可サーバーの認証フォームを指すログインボタン/リンクを使用してページの内容を表示し、存在する場合に実際にデータ取得処理を実行する 2 番目のページにリダイレクトします。

ただし、このコードは次のように変更できます。

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
 set scope="openid profile scope1 scope2"
 set sc=##class(%SYS.OAuth2.Authorization).GetAccessTokenAuthorizationCode(
    ..#OAUTH2APPNAME,scope,..#OAUTH2CLIENTREDIRECTURI,.properties)
 quit +sc
}

この場合は、結果が異なります。 %SYS.OAuth2.Authorization クラスの GetAccessTokenAuthorizationCode メソッドを使用すると、アプリケーションの最初のページの内容を表示せずに、認可サーバーの認証フォームに直接移動します。

これは、Web アプリケーションがモバイルデバイスのネイティブアプリケーションから呼び出され、一部のユーザー情報がネイティブアプリケーション(ランチャー)によってすでに表示されており、認可サーバーを指すボタンを含む Web ページを表示する必要がない場合に便利です。

署名付き JWT トークンを使用する場合は、その内容を検証する必要があります。 この検証は次のメソッドで実行されます。

 set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(applicationName,accessToken,scope,,.jsonObject,.securityParameters,.sc)

メソッドパラメーターの詳細については、Class Reference ドキュメントをご覧ください。

カスタマイズ

OAUTH が認証 / 承認 UI のカスタマイズ用に提供しているオプションについてもう少し説明します。

勤務先のポリシーで、スコープの付与に関してより限定的な動作が要求されているとします。 たとえば、取引先銀行内のさまざまな金融システムに接続するホームバンキングアプリケーションを実行できるとしましょう。 銀行は、取得対象の実際の銀行口座に関する情報を含むスコープへのアクセスのみを許可します。 銀行は非常に多くの口座を運営しているため、すべての口座に静的なスコープを定義することは不可能です。 代わりに、認可処理中にその場でスコープを生成する処理をカスタム認可ページのコードに組み込むことができます。

デモを行うため、ここではサーバー構成にもう 1 つのスコープを追加する必要があります。次の画像を参照してください。

また、%OAuth2.Server.Authenticate.Bank という名前のカスタム Authenticate クラスへの参照も追加しました。

では、銀行の認証クラスはどのようになるのでしょうか? 次は想定されるクラスの例です。 このクラスは、ユーザーが提供するデータを使用して標準の認証フォームと認可フォームを拡張します。 BeforeAuthenticateDisplayPermissionsAfterAuthenticate の各メソッド間を流れる情報は、%OAuth2.Server.Properties クラスの properties 変数によって渡されます。

Class %OAuth2.Server.Authenticate.Bank Extends %OAuth2.Server.Authenticate
{
/// account(口座)のスコープに CUSTOM BESTBANK のサポートを追加します。
ClassMethod BeforeAuthenticate(scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
{
 // 起動スコープが指定されていない場合は何もしません。
 If 'scope.IsDefined("account") Quit $$$OK
 // 起動クエリパラメーターから起動コンテキストを取得します。
 Set tContext=properties.RequestProperties.GetAt("accno")
 // コンテキストがない場合は何もしません。
 If tContext="" Quit $$$OK
    
 try {
    // ここで BestBank コンテキストを照会する必要があります。
    Set tBankAccountNumber=tContext
    // accno のスコープを追加します。 -> 動的にスコープを変更(account なし:<accno> スコープはサーバー構成に存在します)
    // この特定のスコープは、それが Cookie サポートを使用して account または account:accno によって
    // 以前に選択されていた場合に account 経由で同じ accno にアクセスできるようにするために使用されます。
    Do scope.SetAt("Access data for account "_tBankAccountNumber,"account:"_tBankAccountNumber)
    // 処理が終わった account のスコープはもう必要ありません。
    // これにより、account スコープが存在することで DisplayPermissions が強制的に呼び出されるのを防ぎます。
    Do scope.RemoveAt("account")
    
    // AfterAuthenticate が応答プロパティに変換する accno プロパティを追加します。
    Do properties.CustomProperties.SetAt(tBankAccountNumber,"account_number")
 } catch (e) {
    s ^dk("err",$i(^dk("err")))=e.DisplayString()
 }
 Quit $$$OK
}

/// account のスコープに CUSTOM BESTBANK のサポートを追加します。
/// account_number カスタムプロパティが BeforeAuthenticate(account)または
/// DisplayPermissions(account:accno)によって追加された場合は、必要な応答プロパティを追加します。
ClassMethod AfterAuthenticate(scope As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
{
 // account_number(account)または accno(account:accno)プロパティが存在しない限り、ここで実行することは何もありません。
 try {
    // カスタムログを記録する例
    If $$$SysLogLevel>=3 {
     Do ##class(%OAuth2.Utils).LogServerScope("log ScopeArray-CUSTOM BESTBANK",%token)
    }
    If properties.CustomProperties.GetAt("account_number")'="" {
     // 応答に accno クエリパラメーターを追加します。
     Do properties.ResponseProperties.SetAt(properties.CustomProperties.GetAt("account_number"),"accno")
    }
 } catch (e) {
    s ^dk("err",$i(^dk("err")))=e.DisplayString()
 }
 Quit $$$OK
}

/// BEST BANK の account のテキストを含むように変更された DisplayPermissions
ClassMethod DisplayPermissions(authorizationCode As %String, scopeArray As %ArrayOfDataTypes, currentScopeArray As %ArrayOfDataTypes, properties As %OAuth2.Server.Properties) As %Status
{
 Set uilocales = properties.RequestProperties.GetAt("ui_locales")
 Set tLang = ##class(%OAuth2.Utils).SelectLanguage(uilocales,"%OAuth2Login")
 // $$$TextHTML(Text,Domain,Language)
 Set ACCEPTHEADTITLE = $$$TextHTML("OAuth2 Permissions Page","%OAuth2Login",tLang)
 Set USER = $$$TextHTML("User:","%OAuth2Login",tLang)
 Set POLICY = $$$TextHTML("Policy","%OAuth2Login",tLang)
 Set TERM = $$$TextHTML("Terms of service","%OAuth2Login",tLang)
 Set ACCEPTCAPTION = $$$TextHTML("Accept","%OAuth2Login",tLang)
 Set CANCELCAPTION = $$$TextHTML("Cancel","%OAuth2Login",tLang)
 &html<<html>>
 Do ..DrawAcceptHead(ACCEPTHEADTITLE)
 Set divClass = "permissionForm"
 Set logo = properties.ServerProperties.GetAt("logo_uri")
 Set clientName = properties.ServerProperties.GetAt("client_name")
 Set clienturi = properties.ServerProperties.GetAt("client_uri")
 Set policyuri = properties.ServerProperties.GetAt("policy_uri")
 Set tosuri = properties.ServerProperties.GetAt("tos_uri")
 Set user = properties.GetClaimValue("preferred_username")
 If user="" {
    Set user = properties.GetClaimValue("sub")
 }
 &html<<body>>
 &html<<div id="topLabel"></div>>
 &html<<div class="#(divClass)#">>
 If user '= "" {
    &html<
     <div>
     <span id="left" class="userBox">#(USER)#<br>#(##class(%CSP.Page).EscapeHTML(user))#</span>
     >
 }
 If logo '= "" {
    Set espClientName = ##class(%CSP.Page).EscapeHTML(clientName)
   &html<<span class="logoClass"><img src="#(logo)#" alt="#(espClientName)#" title="#(espClientName)#" align="middle"></span>>
 }
 If policyuri '= "" ! (tosuri '= "") {
   &html<<span id="right" class="linkBox">>
    If policyuri '= "" {
     &html<<a href="#(policyuri)#" target="_blank">#(POLICY)#</a><br>>
    }
    If tosuri '= "" {
     &html<<a href="#(tosuri)#" target="_blank">#(TERM)#</a>>
    }
   &html<</span>>
 }
 &html<</div>>
 &html<<form>>
 Write ##class(%CSP.Page).InsertHiddenField("","AuthorizationCode",authorizationCode),!
 &html<<div>>
 If $isobject(scopeArray), scopeArray.Count() > 0 {
    Set tTitle = $$$TextHTML(" is requesting these permissions:","%OAuth2Login",tLang)
   &html<<div class="permissionTitleRequest">>
    If clienturi '= "" {
     &html<<a href="#(clienturi)#" target="_blank">#(##class(%CSP.Page).EscapeHTML(clientName))#</a>>
    } Else {
     &html<#(##class(%CSP.Page).EscapeHTML(clientName))#>
    }
   &html<#(##class(%CSP.Page).EscapeHTML(tTitle))#</div>>
    Set tCount = 0
    Set scope = ""
    For {
     Set display = scopeArray.GetNext(.scope)
     If scope = "" Quit
     Set tCount = tCount + 1
     If display = "" Set display = scope
     Write "<div class='permissionItemRequest'>"_tCount_". "_##class(%CSP.Page).EscapeHTML(display)_"</div>"
    }
 }

 If $isobject(currentScopeArray), currentScopeArray.Count() > 0 {
    Set tTitle = $$$TextHTML(" already has these permissions:","%OAuth2Login",tLang)
   &html<<div>>
   &html<<div class="permissionTitleExisting">>
    If clienturi '= "" {
     &html<<a href="#(clienturi)#" target="_blank">#(##class(%CSP.Page).EscapeHTML(clientName))#</a>>
    } Else {
     &html<#(##class(%CSP.Page).EscapeHTML(clientName))#>
    }
   &html<#(##class(%CSP.Page).EscapeHTML(tTitle))#</div>>
    Set tCount = 0
    Set scope = ""
    For {
     Set display = currentScopeArray.GetNext(.scope)
     If scope = "" Quit
     Set tCount = tCount + 1
     If display = "" Set display = scope
     Write "<div class='permissionItemExisting'>"_tCount_". "_##class(%CSP.Page).EscapeHTML(display)_"</div>"
    }
   &html<</div>>
 }

 /*********************************/
 /*  BEST BANK CUSTOMIZATION      */
 /*********************************/
 try {
    If properties.CustomProperties.GetAt("account_number")'="" {
     // Display the account number obtained from account context.
     Write "<div class='permissionItemRequest'><b>Selected account is "_properties.CustomProperties.GetAt("account_number")_"</b></div>",!

     // or, alternatively, let user add some more information at this stage (e.g. linked account number)
     //Write "<div>Account Number: <input type='text' id='accno' name='p_accno' placeholder='accno' autocomplete='off' ></div>",!
    }
 } catch (e) {
    s ^dk("err",$i(^dk("err")))=e.DisplayString()
 }

 /* original implementation code continues here... */
 &html<
   <div><input type="submit" id="btnAccept" name="Accept" value="#(ACCEPTCAPTION)#"/></div>
   <div><input type="submit" id="btnCancel" name="Cancel" value="#(CANCELCAPTION)#"/></div>
    >
 &html<</form>
 </div>>
 Do ..DrawFooter()
 &html<</body>>
 &html<<html>>
 Quit 1
}

/// CUSTOM BESTBANK の場合、入力された患者を検証する必要があります。
/// ! このメソッドの javascript はユーザーに追加データを DisplayPermissions メソッド内
/// で入力させる場合にのみ必要です !
ClassMethod DrawAcceptHead(ACCEPTHEADTITLE)
{
 &html<<head><title>#(ACCEPTHEADTITLE)#</title>>
 Do ..DrawStyle()
 &html<
 <script type="text/javascript">
 function doAccept()
 {
    var accno = document.getElementById("accno").value;
    var errors = "";
    if (accno !== null) {
     if (accno.length < 1) {
       errors = "Please enter account number name";
     }
    }
    if (errors) {
     alert(errors);
     return false;
    }
    
    // submit the form
    return true;
 }
 </script>
 >
 &html<</head>>
}

}

ご覧のとおり、%OAuth2.Server.Properties クラスにはいくつかの配列が含まれており、渡されています。 具体的には、以下の配列です。

·        RequestProperties – 認可リクエストのパラメーターが含まれています。

·        CustomProperties – 上記のコードの間でデータをやり取りするためのコンテナ。

·        ResponseProperties – トークンリクエストに対する JSON 応答オブジェクトに追加されるプロパティのコンテナ。

·        ServerProperties – 認可サーバーがカスタマイズコードに公開する共有プロパティが含まれます(logo_uri、client_uri など…)

さらに、認可サーバーが返す必要のあるクレームを指定するのに使用されるいくつかの "claims" プロパティが含まれています。

この認証ページを正しく呼び出すため、最初のクライアントページのコードを次のように変更しました。

 set scope="openid profile scope1 scope2 account"
 // このデータはアプリケーションに由来し(フォームデータなど)、リクエストのコンテキストを設定します。
 // ここでは Authenticate クラスをサブクラス化することで、このデータをユーザーに表示することができます。
 // それにより、該当ユーザーはアクセスを許可するかどうかを決めることができます。
 set properties("accno")="75-452152122-5320"
 set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
   ..#OAUTH2APPNAME,
    scope,
   ..#OAUTH2CLIENTREDIRECTURI,
    .properties,
   .isAuthorized,
    .sc)
 if $$$ISERR(sc) {
    write "GetAuthorizationCodeEndpoint Error="
   write ..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!
 }

ご覧のとおり、ここではアプリケーションのさまざまな部分から発生する可能性のあるコンテキスト値を使用して account のスコープとプロパティ配列ノード “accno” を追加しました。 この値はアクセストークンの内部でリソースサーバーに渡され、さらに処理されます。

上記のロジックは、電子的な患者記録を交換するための FHIR 標準で実際に使用されています。

デバッグ

OAUTH フレームワークには、デバッグ機能が組み込まれています。 クライアントとサーバー間の通信はすべて暗号化されているため、これは非常に便利です。 デバッグ機能を使用すると、API クラスによって生成されたトラフィックデータをネットワーク経由で送信する前にキャプチャできます。 コードをデバッグするため、以下のコードに従って単純なルーチンまたはクラスを実装できます。 InterSystems IRIS インスタンスのすべての通信でこのコードを実装する必要があることに注意してください! その場合、OAUTH フローのプロセス内での役割を示す名前をファイル名に指定することをお勧めします。 (以下のサンプルコードは rr.mac ルーチンとして保存されていますが、どんな名前を付けるかはあなた次第です。)

 // d start^rr()
start() public {
 new $namespace
 set $namespace="%sys"
 kill ^%ISCLOG
 set ^%ISCLOG=5
 set ^%ISCLOG("Category","OAuth2")=5
 set ^%ISCLOG("Category","OAuth2Server")=5
 quit
}

 // d stop^rr()
stop() public {
 new $namespace
 set $namespace="%sys"
 set ^%ISCLOG=0
 set ^%ISCLOG("Category","OAuth2")=0
 set ^%ISCLOG("Category","OAuth2Server")=0
 quit

}

 // display^rr()
display() public {
 new $namespace
 set $namespace="%sys"
 do ##class(%OAuth2.Utils).DisplayLog("c:\temp\oauth2_auth_server.log")
 quit
}

次に、テストを開始する前にターミナルを開き、すべての InterSystems IRIS ノード(クライアント、認可サーバー、リソースサーバー)で d start^rr() を呼び出してください。 完了後、d stop^rr() d display^rr() を実行してログファイルを読み込んでください。

最後に

この連載記事では、InterSystems IRIS OAuth 2.0 の実装を使用する方法を学びました。 パート1では簡単なクライアントアプリケーションのデモを行い、パート2では複雑な例を説明しました。 最後に、OAuth 2.0 の実装で最も重要なクラスについて説明し、ユーザーアプリケーション内でそれらを呼び出す必要がある場合について説明しました。

時々私が投げかけるくだらない質問に我慢強く回答し、この連載をレビューしてくれた Marvin Tener に心から感謝の意を表します。

 

0
0 374
記事 Shintaro Kaminaka · 8月 20, 2020 23m read

作成者:Daniel Kutac(InterSystems セールスエンジニア) 注意: 使用されている URL に戸惑っている方のために。*元の連載記事では、dk-gs2016 と呼ばれるマシンの画面を使用していました。 新しいスクリーンショットは別のマシンから取得されています。 *WIN-U9J96QBJSAG という URL は dk-gs2016 であると見なしても構いません。

パート2. 認可サーバー、OpenID Connect サーバー

この短い連載の前のパートでは、OAUTH[1] クライアントとして機能する単純な使用事例について学びました。 今回は私たちの経験をまったく新しいレベルに引き上げましょう。 InterSystems IRIS がすべての OAUTH の役割を果たす、より複雑な環境を構築します。 クライアントの作成方法はすでに分かっていますので、認可サーバーだけでなく、OpenID Connect[2] プロバイダーにも注意を向けましょう。 前のパートと同様に、環境を準備する必要があります。 今回はより多くの変動要素があるため、より注意を要します。

具体例を見る前に、OpenID Connect について少し説明する必要があります。 前のパートの内容を覚えていらっしゃるかと思いますが、Google から認可してもらうため、まずは自身がGoogle で認証を受けることを求められていました。 認証は OAUTH フレームワークには含まれていません。 実際、OAUTH に依存しない多くの認証フレームワークがあります。 そのうちの1つに OpenID と呼ばれるものがあります。 当初は単独の構想で開始されましたが、最近では OAUTH フレームワークによって提供されるインフラストラクチャ、つまり通信とデータ構造が活用されています。 その結果、OpenID Connect が誕生しました。 事実、多くの人がこれを OAUTH の強化版と呼んでいます。 実際、OpenID Connect を使用すると、認可するだけでなく、OAUTH フレームワークのよく知られたインターフェースを使用して認証することもできます。

複雑な OpenID Connect のデモ

ここでは、パート 1 のクライアントコードの多くを活用します。 そうすることで多くの手間が省けるため、環境のセットアップに集中できます。

前提条件

今回は、SSL が有効になっている既存の Web サーバーに PKI インフラストラクチャを追加する必要があります。 OpenID Connect の要件である暗号化が必要です。 ユーザー認証が必要な場合は、第三者がエージェント(クライアント、認証サーバーなど)になりすますし、ネットワーク経由でユーザーの機密データを送信できないようにする必要があります。 そこで X.509 ベースの暗号化の出番です。

注意 : Cache 2017.1 以降では、X.509 証明書を使用して JWT / JWKS(JSON Web Key Set)を生成する必要はありません。 ここでは下位互換性と単純化を図るためにこのオプションを使用しています。
### PKI 厳密に言えば、Caché PKI インフラストラクチャを使用する必要はまったくありません。ただし、openssl などのツールを直接使用してすべての証明書を生成するよりは楽です。 詳細は InterSystems IRIS の[ドキュメント](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCAS_pki#GCAS_C157792)や他の場所でも確認できますので、ここでは証明書の生成に関する詳細は触れません。 証明書を生成した結果、3 つの公開鍵/秘密鍵のペアと関連する証明書が作成されます。 これらを次のように呼びます。
  • 発行元認証局の root_ca(root_ca.cer)

  • 認可サーバーおよび OpenID サーバー用の auth(auth.cer および auth.key)

  • クライアントアプリケーションサーバー用の client(client.cer および client.key)

X.509 資格情報

デモ中に交換された JSON Web Token(JWT)に署名して検証できるよう、個々のサーバーで X.509 資格情報を定義する必要があります。

認可サーバーと認証サーバーの構成

ここでは X.509 資格情報の定義方法については詳しく説明せず、AUTHSERVER インスタンスの資格情報のスクリーンショットを示します。 画像のように、AUTHSERVER にはその秘密鍵と証明書がありますが、CLIENT に関しては公開鍵を含む証明書しかありません。

クライアントサーバーの構成

同様に、CLIENT インスタンスで資格情報が定義されています。 ここで、CLIENT には秘密鍵と証明書がありますが、AUTHSERVER に関しては公開鍵を含む証明書しかありません。

リソースサーバーの構成

このセットアップの例では、RESSERVER インスタンスで X509 資格情報を定義する必要はありません。

OAUTH の構成

この連載のパート 1 で説明した構成と同様に、サーバーを OAUTH 用に構成する必要があります。 まずは、OAUTH 構成全体の中心的なコンポーネントである AUTHSERVER インスタンスから始めましょう。

AUTHSERVER

System Management Portal で、System Administration > Security > OAuth 2.0 > Server Configuration を開きます。 メニューのリンクをクリックし、次のフォーム項目に入力します。

  • Host name(ホスト名)

  • Port(ポート、省略可)

  • Prefix(プレフィックス、省略可) – これら 3 つのフィールドは Issuer endpoint(発行者エンドポイント)を構成します。

  • 更新トークンを返す条件を指定します。

  • Supported grant types(サポートされているグラント種別)をチェックします。このデモでは 4 つの種別すべてにチェックします。 ただし、認可コードのみが使用されます。

  • 必要に応じて Audience required(オーディエンスを要求)をチェックします。これにより、aud プロパティが認可コードと暗黙的な許可(Implicit)のリクエストに追加されます。

  • 必要に応じて Support user session(ユーザーセッションをサポート)をチェックします。これにより、認可サーバーが現在このブラウザを使用しているユーザーのログイン状態を維持するために httpOnly Cookie が使用されます。 2 回目以降のアクセストークンの要求では、ユーザー名とパスワードの入力は求められません。

  • Endpoint intervals(エンドポイントの間隔)を指定します。

  • このサーバーでサポートされるスコープ(Supportted scopes)を定義します。

  • Customization Options(カスタマイズオプション)のデフォルト値を受け入れるか、カスタム値を入力します。**注意: **JWT が単なる opaque トークンではなくアクセストークンとして使用されるよう、Generate token class(生成トークンクラス)の値を %OAuth2.Server.Generate から %OAuth2.Server.JWT に変更してください。

  • OAuth 2.0 の要求に従い、HTTP 上の SSL を確立するための登録済み SSL 構成の名前を入力します。

  • JSON Web Token(JWT)の設定を入力します。

以下はサンプル構成のスクリーンショットです。 サーバー構成を定義したら、サーバークライアント構成を入力する必要があります。 サーバー構成フォームのページ内で、「Client Configurations」(クライアント構成)ボタンをクリックし、CLIENT インスタンスおよび RESSERVER インスタンスの「Create New Configuration」(新しい構成の作成)をクリックします。 (IRISを使用して構成している場合は、「クライアントディスクリプション」の構成ページから以下の設定を行います。) 以下の画像は CLIENT の構成を示しています。 JWT Token(JWT トークン)タブはデフォルト値である空のままにします。 ご覧のとおり、実際のアプリケーションの場合とは異なり、フィールドには無意味なデータが入力されています。 同様に、RESSERVER の構成を示します。 ご覧のとおり、リソースサーバーに必要なのは非常に基本的な情報だけです。具体的には、Client type(クライアント種別)をリソースサーバーに設定する必要があります。 CLIENT では、より詳細な情報とクライアント種別を入力する必要があります(クライアントはクライアントシークレットをサーバーで維持し、クライアントエージェントには送信しない Web アプリケーションとして動作するため、機密(confidential)を選択します)。

CLIENT

SMP で、System Administration > Security > OAuth 2.0 > Client Configurations を開きます。 「Create Server Configuration」(サーバー構成の作成)ボタンをクリックし、フォームに入力して保存します。 Issuer Endpoint(発行者エンドポイント)が、AUTHSERVER インスタンスで前に定義した値に対応していることを必ず確認してください! また、認可サーバーのエンドポイントを Web サーバーの構成に従って変更する必要があります。 この場合は各入力フィールドに「authserver」を埋め込んだだけです。 次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成の作成)ボタンをクリックします。 以上です! ここまでの手順で CLIENT と AUTHSERVER の両方を構成しました。 多くの使用事例ではこれだけで十分です。リソースサーバーは単なる AUTHSERVER のネームスペースにすぎず、結果的にすでに保護されている場合があるからです。 しかし、外部の医師が内部の臨床システムからデータを取得しようとしている使用事例に対応する場合を考えてみましょう。 このような医師がデータを取得できるようにするため、この医師のアカウント情報を監査目的と法医学的な理由でリソースサーバー内に確実に保存したいと思います。 この場合は、続けて RESSERVER を定義する必要があります。

RESSERVER

SMP で、System Administration > Security > OAuth 2.0 > Client Configurations を開きます。 「Create Server Configuration」(サーバー構成の作成)ボタンをクリックし、フォームに入力して保存します。 ここでは、Cache 2017.1 に実装された新機能である検出機能を使用しました。 ご覧のように、この構成は CLIENT インスタンスの対応する構成と同じデータを使用しています。 次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成の作成)ボタンをクリックします。 X.509 資格情報から JWT を作成することはお勧めしませんが、ここでは互換性のために使用しました。 CLIENTとほとんど同じ手順でしたが、必要なものでした。 しかし、これで先に進んでコーディングできるようになりました!

クライアントアプリケーション

話をできる限り簡単にするため、パート1で説明した Google の例から多くのコードをリサイクルします。 クライアントアプリケーションは /csp/myclient で実行されるわずか 2 つの CSP ページからなるアプリケーションです。セキュリティは強制せず、未認証ユーザーとして実行されます。

ページ 1

Class Web.OAUTH2.Cache1N Extends %CSP.Page
{

Parameter OAUTH2CLIENTREDIRECTURI = "https://dk-gs2016/client/csp/myclient/Web.OAUTH2.Cache2N.cls";

Parameter OAUTH2APPNAME = "demo client";

ClassMethod OnPage() As %Status
{
  &html<<html>

<body>
  <h1>Cache&acute; OAuth2 プロバイダーに対する認証と認可</h1>
  <p>このページのデモでは、OAuth2 の認可を使用して Cache&acute; API 関数を呼び出す方法を示しています。
  <p>ここでは Cache&acute; の認証および認可サーバーを呼び出し、アプリケーションに別の Cache&acute; サーバーに保存されているデータへのアクセスを許可します。
 >

  // 適切なリダイレクトとスコープを持つ認証エンドポイントの URL を取得します。
  // 返された URL は下のボタンで使用されます。

  // DK: 'dankut' アカウントを使用して認証します!
  set scope="openid profile scope1 scope2"
  set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
    ..#OAUTH2APPNAME,
    scope,
    ..#OAUTH2CLIENTREDIRECTURI,
    .properties,
    .isAuthorized,
    .sc)
  if $$$ISERR(sc) {
    write "GetAuthorizationCodeEndpoint Error="
    write ..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!
  } 

  &html<
  <div class="portalLogoBox"><a class="portalLogo" href="#(url)#">Authorize for <b>ISC</b></a></div>
  </body></html>>
  Quit $$$OK
}

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
  #dim %response as %CSP.Response
  set scope="openid profile scope1 scope2"
  if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) {
    set %response.ServerSideRedirect="Web.OAUTH2.Cache2N.cls"
  }
  quit 1
}

}

ページ 2

Class Web.OAUTH2.Cache2N Extends %CSP.Page
{

Parameter OAUTH2APPNAME = "demo client";

Parameter OAUTH2ROOT = "https://dk-gs2016/resserver";

Parameter SSLCONFIG = "SSL4CLIENT";

ClassMethod OnPage() As %Status
{
    &html<<html>
  
<body>>
    
    // OAuth2 サーバーからのアクセストークンがあるかどうかをチェックします。
    set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1 scope2",.accessToken,.idtoken,.responseProperties,.error)
    
    // アクセストークンがあるかどうかをさらにチェックします。
    // 以下は実行可能なすべてのテストであり、あらゆるケースで必要なわけではありません。
    // 各テストで返される JSON オブジェクトが単に表示されています。
    if isAuthorized {
        write "<h3>Authorized!</h3>",!
        
        
        // JWT の場合、検証してからアクセストークンから詳細を取得します。
        set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,accessToken,"scope1 scope2",,.jsonObject,.securityParameters,.sc)
        if $$$ISOK(sc) {
            if valid {
                write "Valid JWT"_"<br>",!    
            } else {
                write "Invalid JWT"_"<br>",!    
            }
            write "Access token="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "JWT Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!

        // イントロスペクションエンドポイントを呼び出して結果を表示します。RFC 7662 を参照してください。
        set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.jsonObject)
        if $$$ISOK(sc) {
            write "Introspection="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "Introspection Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
        
        if idtoken'="" {
            // ID トークンの検証と表示。OpenID Connect Core の仕様を参照してください。
            set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
                ..#OAUTH2APPNAME,
                idtoken,
                accessToken,,,
                .jsonObject,
                .securityParameters,
                .sc)
            if $$$ISOK(sc) {
                if valid {
                    write "Valid IDToken"_"<br>",!    
                } else {
                    write "Invalid IDToken"_"<br>",!    
                }
                write "IDToken="
                do jsonObject.%ToJSON()
                write "<br>",!
            } else {
                write "IDToken Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
            }
        } else {
            write "No IDToken returned"_"<br>",!
        }
        write "<br>",!
    
        // アプリケーションロジックには不要ですが、委任認証に渡すことができるユーザーに関する情報を提供します。
    
        // Userinfo エンドポイントを呼び出して結果を表示します。OpenID Connect Core の仕様を参照してください。
        set sc=##class(%SYS.OAuth2.AccessToken).GetUserinfo(
            ..#OAUTH2APPNAME,
            accessToken,,
            .jsonObject)
        if $$$ISOK(sc) {
            write "Userinfo="
            do jsonObject.%ToJSON()
            write "<br>",!
        } else {
            write "Userinfo Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<p>",!

        /***************************************************
        *                                                  *
        *   リソースサーバーを呼び出し、結果を表示します。   *
        *                                                  *
        ***************************************************/
                
        // オプション 1 - リソースサーバー - 定義によれば認可サーバーからのデータを信頼します。
        //     そのため、リソースサーバーに渡されたアクセストークンが有効である限り
        //  要求元を問わずデータを提供します。
        
        // オプション 2 - または委任認証(OpenID Connect)を使用して 
        //  (委任認証を保護して)別の CSP アプリケーションを呼び出すこともできます。
        //  - これはまさにこのデモで実施する内容です。
        
        
        write "<h4>リソースサーバーの呼び出し(委任認証)","</h4>",!
        set httpRequest=##class(%Net.HttpRequest).%New()
        // AddAccessToken は現在のアクセストークンをリクエストに追加します。
        set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
            httpRequest,,
            ..#SSLCONFIG,
            ..#OAUTH2APPNAME)
        if $$$ISOK(sc) {
            set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio/oauth2test.demoResource.cls")
        }
        if $$$ISOK(sc) {
            set body=httpRequest.HttpResponse.Data
            if $isobject(body) {
                do body.Rewind()
                set body=body.Read()
            }
            write body,"<br>",!
        }
        if $$$ISERR(sc) {
            write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
    
        write "<h4>Call resource server - no auth, just token validity check","</h4>",!
        set httpRequest=##class(%Net.HttpRequest).%New()
        // AddAccessTokenは現在のアクセストークンをリクエストに追加します。
        set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
            httpRequest,,
            ..#SSLCONFIG,
            ..#OAUTH2APPNAME)
        if $$$ISOK(sc) {
            set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio2/oauth2test.demoResource.cls")
        }
        if $$$ISOK(sc) {
            set body=httpRequest.HttpResponse.Data
            if $isobject(body) {
                do body.Rewind()
                set body=body.Read()
            }
            write body,"<br>",!
        }
        if $$$ISERR(sc) {
            write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"<br>",!    
        }
        write "<br>",!
    } else {
        write "Not Authorized!<p>",!
        write "<a href='Web.OAUTH2.Cache1N.cls'>Authorize me</a>"
    }    
    &html<</body></html>>
    Quit $$$OK
}

}

次のスクリーンショットで処理を説明します。

AUTHSERVER インスタンスの認可 / OpenID Connect 認証サーバーのログインページ

AUTHSERVER のユーザー同意ページ

その後、最終的に結果ページが表示されます。

ご覧のとおり、実際にコードを読んでもパート 1 で示したクライアントのコードとほとんど違いはありません。 ページ 2 には違いがあります。 これはデバッグ情報であり、JWT の有効性をチェックしています。 返ってきた JWT を検証した後、AUTHSERVER からのユーザー識別情報に関するデータに対してイントロスペクションを実行できました。 ここではこの情報をページに出力しただけですが、それ以上のこともできます。 上記で説明した外部の医師の使用事例と同様に、必要に応じて識別情報を使用し、それを認証目的でリソースサーバーに渡すことができます。 または、この情報をパラメーターとしてリソースサーバーへの API 呼び出しに渡すこともできます。

次の段落では、ユーザー識別情報の使用方法について詳しく説明します。

リソースアプリケーション

リソースサーバーは認可 / 認証サーバーと同じサーバーにすることができ、多くの場合はそうなります。 しかし、このデモでは 2 つのサーバーを独立した InterSystems IRIS インスタンスにしました。

したがって、リソースサーバーでセキュリティコンテキストを使用する方法には 2 つあります。

方法 1 – 認証なし

これは最も単純なケースです。 認可 / 認証サーバーはまったく同じ Caché インスタンスです。 この場合、単一の目的のために特別に作成された csp アプリケーションにアクセストークンを渡すだけで、OAUTH を使用するクライアントアプリケーションにデータを提供し、データを要求することを認可できます。

リソース csp アプリケーションの構成(ここでは /csp/portfolio2 と呼びました)は、以下のスクリーンショットのようになります。

最小限のセキュリティをアプリケーション定義に組み込み、特定の CSP ページのみを実行できるようにします。

または、リソースサーバーは従来の Web ページの代わりに REST API を提供できます。 実際のシナリオでは、セキュリティコンテキストを微調整するのはユーザー次第です。

ソースコードの例:

Class oauth2test.demoResource Extends %CSP.Page
{

ClassMethod OnPage() As %Status
{
    set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
    if $$$ISOK(sc) {
        set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
        if $$$ISOK(sc) {        
            // 必要に応じて jsonObject のフィールドを検証します

            w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio2</i> application!</h3>"
            w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
        }
    } else {
        w "<h3>NOT AUTHORIZED!</h3>"    
        w "<pre>"
        w
        i $d(%objlasterror) d $system.OBJ.DisplayError()
        w "</pre>"
    }
    Quit $$$OK
}

}

方法 2 – 委任認証

これはもう 1 つの極端なケースです。ユーザーがリソースサーバーの内部ユーザーと同等のセキュリティコンテキストで作業しているかのように、リソースサーバーでユーザーの識別情報を可能な限り最大限に活用したいと考えています。

解決方法の 1 つは、委任認証を使用することです。

この方法を実行するには、さらにいくつかの手順を実行してリソースサーバーを構成する必要があります。

·        委任認証を有効にする

·        ZAUTHENTICATE ルーチンを提供する

·        Web アプリケーションを構成する(この場合、/csp/portfolio で呼び出しました)

ここではユーザー識別情報とそのスコープ(セキュリティプロファイル)を提供した AUTHSERVER を信頼しているため、ZAUTHENTICATE ルーチンの実装は非常に単純で簡単です。そのため、ここではいかなるユーザー名も受け入れ、それをスコープと共にリソースサーバーのユーザーデータベースに渡しています(OAUTH スコープと InterSystems IRIS のロール間で必要な変換を行ったうえで)。 それだけです。 残りの処理は InterSystems IRIS によってシームレスに行われます。

これは ZAUTHENTICATE ルーチンの例です。

#include %occErrors
#include %occInclude

ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC
{
    set tRes=$SYSTEM.Status.OK()
    try {        
        set Properties("FullName")="OAuth account "_Username
        //set Properties("Roles")=Credentials("scope")
        set Properties("Username")=Username
        //set Properties("Password")=Password
        // Credentials 配列を GetCredentials() メソッドから渡せないため、一時的に書き換えます。
        set Properties("Password")="xxx"    // OAuth2 アカウントのパスワードは気にしません。
        set Properties("Roles")=Password
    } catch (ex) {
        set tRes=$SYSTEM.Status.Error($$$AccessDenied)
    }
    quit tRes
}

GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public 
{
    s ts=$zts
    set tRes=$SYSTEM.Status.Error($$$AccessDenied)        

     try {
         If ServiceName="%Service_CSP" {
            set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
            if $$$ISOK(sc) {
                set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
                if $$$ISOK(sc) {
                    // ToDo: 標準アカウントと委任されたアカウント(OpenID)が競合する可能性があるため、注意してください!
                    set Username=jsonObject.username
                    set Credentials("scope")=$p(jsonObject.scope,"openid profile ",2)
                    set Credentials("namespace")=Namespace
                    // temporary hack
                    //set Password="xxx"
                    set Password=$tr(Credentials("scope")," ",",")
                    set tRes=$SYSTEM.Status.OK()
                } else {
                    set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed) 
                }
            }    
        } else {
            set tRes=$SYSTEM.Status.Error($$$AccessDenied)        
        }
     } catch (ex) {
         set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed)
    }
    Quit tRes
}

CSP ページ自体は非常にシンプルになります。

Class oauth2test.demoResource Extends %CSP.Page
{

ClassMethod OnPage() As %Status
{
    // アクセストークン認証は委任認証によって実行されます!
    // もう一度ここで行う必要はありません。

    // これはリクエストからアクセストークンを取得し、イントロスペクションエンドポイントを
    // 使用してアクセストークンの有効性を確認するダミーのリソースサーバーです。
    // 通常、応答はセキュリティに関連しませんが、リクエストパラメーターに基づく
    // 興味深いデータが含まれている可能性があります。
    w "<p><h3>Hello from Cach&eacute; server: <i>/csp/portfolio</i> application!</h3>"
    w "<p>running code as <b>$username = "_$username_"</b> with following <b>$roles = "_$roles_"</b> at node <b>"_$p($zu(86),"*",2)_"</b>."
    Quit $$$OK
}

}

そして最後に、/csp/portfolio の Web アプリケーション構成を示します。

あなたが本当に心配であったなら、最初のバリエーションで行ったように _Permitted クラス_を設定できたかもしれません。 または、REST API を使用していたかもしれません。 しかし、これらの設定に関してはこの記事の中では説明しません。

次回は、InterSystems IRIS OAUTH フレームワークによって導入される個々のクラスについて説明します。 それらの API、およびそれらを呼び出すタイミングと場所について説明します。

 

[1] この記事で OAUTH について言及する場合、常に RFC 6749(https://tools.ietf.org/html/rfc6749)で規定されている OAuth 2.0 を指しています。 ここでは簡略化のために短縮形の OAUTH を使用しています。

[2] OpenID Connect は OpenID Foundation(http://openid.net/connect)によって管理されています。

0
0 446
記事 Shintaro Kaminaka · 7月 3, 2020 17m read

この記事と後続の2つの連載記事は、InterSystems製品ベースのアプリケーションでOAuth 2.0フレームワーク(簡略化のためにOAUTHとも呼ばれます)を使用する必要のある開発者またはシステム管理者向けのユーザーガイドを対象としています。 

作成者:Daniel Kutac(InterSystemsシニアセールスエンジニア) 

公開後の修正および変更の履歴 

  • 2016年8月3日 - 新しいバージョンのページを反映するため、Googleのクライアント設定のスクリーンショットを修正し、Google APIのスクリーンショットを更新しました。
  • 2016年8月28日 - Cache 2016.2でのJSON対応への変更を反映するため、JSON関連コードを変更しました。 
  • 2017年5月3日 - Cache 2017.1でリリースされた新しいUIと機能を反映するため、テキストと画面を更新しました。 
  • 2018年2月19日 - 最新の開発内容を反映するために、CachéをInterSystems IRISに変更しました。 製品名は変更されていますが、この記事はすべてのInterSystems製品(InterSystems IRIS Data Platform、Ensemble、Caché)を対象としています。 

パート1. クライアント 

概要 

これは、3部構成のInterSystemsによるOpen Authorization Frameworkの実装に関する連載記事の最初の記事です。 

この最初のパートでは、このトピックについて簡単に紹介し、InterSystems IRISアプリケーションが認証サーバーのクライアントとして機能し、保護されたリソースを要求する簡単なシナリオを示します。 

パート2ではより複雑なシナリオについて説明します。そこではInterSystems IRIS自体が認証サーバーとして機能するほか、OpenID Connectを介した認証サーバーとしても機能します。 

このシリーズの最後のパートでは、OAUTHフレームワーククラスの個々の部分について説明します。それらはInterSystems IRISにより実装されているからです。 

Open Authorization Framework[1]とは 

皆さんの多くはすでにOpen Authorization Frameworkとその使用目的について聞いたことがあるかと思います。 そのため、この記事では初めて同フレームワークを耳にした方のために簡単な要約を掲載します。 

現在はバージョン2.0であるOpen Authorization Framework(OAUTH)は、クライアント(データを要求するアプリケーション)とリソース所有者(要求されたデータを保持するアプリケーション)の間に間接的な信頼を確立することにより、主にWebベースのアプリケーションが安全な方法で情報を交換できるようにするプロトコルです。 この信頼自体は、クライアントとリソースサーバーの両方が認識して信頼する機関によって提供されます。 この機関は認証サーバーと呼ばれます。 

次の事例を使用して簡単に説明します。 

Jenny(OAUTH用語ではリソース所有者)がJennyCorp社のプロジェクトに取り組んでいるとします。 彼女はより大きな潜在的なビジネスのプロジェクト計画を作成し、JohnInc社のビジネスパートナーであるJohn(クライアントユーザー)にドキュメントのレビューを依頼します。 ただし、彼女はジョンに自社のVPNへのアクセスを許可することを快く思っていないので、ドキュメントをGoogleドライブ(リソースサーバー)または他の同様のクラウドストレージに置いています。 そうすることで、彼女は彼女とGoogle(認証サーバー)の間に信頼関係を確立していました。 彼女はJohnと共有するドキュメントを選びます(JohnはすでにGoogleドライブを使用しており、Jennyは彼のメールアドレスを知っています)。 

Johnはドキュメントを閲覧したいときには自分のGoogleアカウントで認証し、モバイルデバイス(タブレットやノートパソコンなど)からドキュメントエディタ(クライアントサーバー)を起動し、Jennyのプロジェクトファイルを読み込みます。 

とてもシンプルに聞こえますが、2人とGoogleの間には多くの通信が発生しています。 どの通信もOAuth 2.0仕様に準拠しているため、Johnのクライアント(リーダーアプリケーション)は最初にGoogleで認証する必要があります(OAUTHはこのステップに対応していません)。ジョンがGoogleが提供するフォームにJohnが同意して認証すると、Googleはアクセストークンを発行し、リーダーアプリケーションにドキュメントへのアクセスを許可します。 リーダーアプリケーションはアクセストークンを使用してGoogleドライブにリクエストを発行し、Jennyのファイルを取得します。 

以下の図に、個々の当事者間の通信を示しています。 

注意: どのOAUTH 2.0通信もHTTPリクエストを使用していますが、サーバーは必ずしもWebアプリケーションである必要はありません。 

InterSystems IRISを使ってこの簡単なシナリオを説明しましょう。 

簡単なGoogleドライブのデモ 

このデモでは、私たち自身のアカウントを使ってGoogleドライブに保存されているリソース(ファイルのリスト)をリクエストする小さなCSPベースのアプリケーションを作成します(ついでにカレンダーのリストも取得します)。 

前提条件 

アプリケーションのコーディングを始める前に、環境を準備する必要があります。 この環境には、SSLが有効になっているWebサーバーとGoogleのプロファイルが含まれます。 

Webサーバーの構成 

上記のように、認証サーバーとはSSLを使用して通信する必要があります。これは、OAuth 2.0がデフォルトでSSLを要求するためです。 データを安全に保つためには必要なことですよね? 

この記事ではSSLをサポートするようにWebサーバーを構成する方法は説明しませんので、お好みの各Webサーバーのユーザーマニュアルを参照してください。 皆さんの好奇心をそそるため、この詳細な例ではMicrosoft IISサーバーを使用します(後でいくつかのスクリーンショットを掲載するかもしれません)。 

Googleの構成 

Googleに登録するには、Google API Manager(https://console.developers.google.com/apis/library?project=globalsummit2016demo)を使用する必要があります 

デモのために、GlobalSummit2016Demoというアカウントを作成しました。 Drive APIが有効になっていることを確認してください。 

次に、認証情報を定義します。 

次の点に注意してください。 

承認済みのJavaScript生成元 – 呼び出し元のページに対し、ローカルで作成されたスクリプトのみを許可します。 

承認済みのリダイレクトURI – 理論上はクライアントアプリケーションを任意のサイトにリダイレクトできますが、InterSystems IRISのOAUTH実装を使用する場合は https://localhost/csp/sys/oauth2/OAuth2.Response.cls にリダイレクトする必要があります。 スクリーンショットのように複数の承認済みのリダイレクトURIを定義できますが、このデモでは2つのうち2番目のエントリのみが必要です。 

最後に、InterSystems IRISをGoogle認証サーバーのクライアントとして構成する必要があります。 

Cachéの構成 

InterSystems IRIS OAUTH2クライアントの構成は2段階で行われます。 まず、サーバー構成を作成する必要があります。 

SMPで、System Administration(システム管理) > Security(セキュリティ) > OAuth 2.0 > Client Configurations(クライアント構成)を開きます。 

「サーバー構成の作成」ボタンをクリックし、フォームに入力して保存します。 

フォームに入力したすべての情報は、Google Developers Consoleのサイトで確認できます。 InterSystems IRISはOpen IDの自動検出に対応しています。 ただし、ここでは自動検出を使用せず、すべての情報を手動で入力しました。 

次に、新しく作成された発行者エンドポイントの横にある「Client Configurations」(クライアント構成)リンクをクリックし、「Create Client Configuration」(クライアント構成を作成する)ボタンをクリックします。 

「Client Information」(クライアント情報)タブと「JWT Settings」(JWT設定)タブは空のままにし(デフォルト値を使用します)、クライアントの認証情報を入力してください。 

注意:ここでは、Confidential Clientを作成しています。これはPublic Clientよりも安全であり、クライアントシークレットがクライアントサーバーアプリケーションを離れることはありません(ブラウザに送信されません)。 

また、「Use SSL/TLS」(SSL/TLSを使用する)がチェックされ、ホスト名(ここではクライアントアプリケーションにローカルにリダイレクトしているため、localhostにします)が入力され、さらにはポートとプレフィックスが入力されていることを確認してください(これは同じマシンに複数のInterSystems IRISがある場合に役立ちます)。 入力した情報に基づいてクライアントリダイレクトURLが生成され、上の行に表示されます。 

上のスクリーンショットでは、GOOGLEという名前のSSL構成を選択しました。 この名前自体は、多くのSSL構成のうちどれをこの特定の通信チャネルで使用するかを決定するためにのみ使用されます。 CachéはSSL/TLS構成を使用し、サーバー(この場合はGoogle OAuth 2.0 URI)との安全なトラフィックを確立するために必要なすべての情報を保存しています。 

より詳細な説明については、ドキュメントを参照してください。 

Googleの認証情報定義フォームから取得したクライアントIDとクライアントシークレットの値を入力します(手動構成を使用する場合)。 

これですべての構成ステップが完了し、CSPアプリケーションのコーディングに進むことができます。 

クライアントアプリケーション 

クライアントアプリケーションは、シンプルなWebベースのCSPアプリケーションです。 そのため、Webサーバーによって定義および実行されるサーバー側のソースコードと、Webブラウザによってユーザーに公開されるユーザーインターフェイスで構成されています。 

クライアントサーバー 

クライアントサーバーは単純な2ページのアプリケーションです。 アプリケーション内では次の処理を実行します。 

·        Google認証サーバーのリダイレクトURLを組み立てます。 

·        Google Drive APIおよびGoogle Calendar APIへのリクエストを実行し、結果を表示します。 

ページ1 

これはアプリケーションの1ページであり、そのリソースについてGoogleを呼び出すことにしました。 

以下はこのページを表す最小限の、しかし完全に動作するコードです。 

Class Web.OAUTH2.Google1N Extends %CSP.Page 

{ 


Parameter OAUTH2CLIENTREDIRECTURI = "https://localhost/csp/google/Web.OAUTH2.Google2N.cls"; 


Parameter OAUTH2APPNAME = "Google"; 


ClassMethod OnPage() As %Status 

{ 

  &html<<html> 

<head> 

</head> 

<body style="text-align: center;"> 

        <!-- ページの内容をここに挿入します --> 

        <h1>Google OAuth2 API</h1> 

        <p>このページのデモでは、OAuth2認証を使用してGoogle API関数を呼び出す方法を示しています。 

        <p>ユーザーとユーザーのGoogleドライブのファイル、およびカレンダーエントリに関する情報を取得します。 

        > 

         

  // Googleで認証するにはopenidのスコープを指定する必要があります 

  set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 

  "https://www.googleapis.com/auth/userinfo.profile "_ 

  "https://www.googleapis.com/auth/drive.metadata.readonly "_ 

  "https://www.googleapis.com/auth/calendar.readonly" 


  set properties("approval_prompt")="force" 

  set properties("include_granted_scopes")="true" 


  set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(..#OAUTH2APPNAME,scope, 

    ..#OAUTH2CLIENTREDIRECTURI,.properties,.isAuthorized,.sc)  

  w !,"<p><a href='"_url_"'><img border='0' alt='Googleサインイン' src='images/google-signin-button.png' ></a>"  


      &html<</body> 

</html>> 

  Quit $$$OK 

} 


ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ] 

{ 

  #dim %response as %CSP.Response 

  set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 

    "https://www.googleapis.com/auth/userinfo.profile "_ 

    "https://www.googleapis.com/auth/drive.metadata.readonly "_ 

    "https://www.googleapis.com/auth/calendar.readonly" 

  if ##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) { 

    set %response.ServerSideRedirect="Web.OAUTH2.Google2N.cls" 

  } 

  quit 1 

  } 

} 

以下にこのコードの簡単な説明を記します。 

1.     OnPreHTTPメソッド - まず、すでに有効なアクセストークンをGoogleの認証結果として取得しているかどうかを確認します。この認証は、例えば単にページを更新したときに発生する可能性があります。 トークンを取得できていない場合は、認証する必要があります。 トークンを取得できている場合は、結果表示ページにページをリダイレクトするだけです。 

2.      OnPageメソッド - 有効なアクセストークンがない場合にのみここに到達します。その場合、認証してGoogleに対する権限を付与し、アクセストークンを付与してもらうために通信を開始しなければなりません。 

3.      Google認証ダイアログの動作を変更するスコープ文字列とプロパティ配列を定義します(私たちのIDに基づいて認証する前に、Googleに対して認証する必要があります)。 

4.      最後にGoogleのログインページのURLを受け取り、それをユーザーに提示してから同意ページを表示します。 

追加の注意事項: 

ここでは実際のリダイレクトページを https://www.localhost/csp/google/Web.OAUTH2.Google2N.cls(OAUTH2CLIENTREDIRECTURIパラメータ内)で指定しています。 ただし、Google認証情報の定義ではInterSystems IRIS OAUTH Frameworkのシステムページを使用しています。 リダイレクトは、OAUTHハンドラークラスによって内部的に処理されます。 

ページ2 

このページにはGoogle認証の結果が表示されます。成功した場合はGoogleのAPIコールを呼び出してデータを取得します。 繰り返しになりますが、このコードは最小限でも完全に機能します。 受信データがどのような構造で表示されるかは、皆さんのご想像にお任せします。 

Include %occInclude 


Class Web.OAUTH2.Google2N Extends %CSP.ページ 

{ 


Parameter OAUTH2APPNAME = "Google"; 


Parameter OAUTH2ROOT = "https://www.googleapis.com"; 


ClassMethod OnPage() As %Status 

{ 

  &html<<html> 

   <head> 

   </head> 

   <body>> 


  // アクセストークンがあるかどうかを確認します 

  set scope="openid https://www.googleapis.com/auth/userinfo.email "_ 

    "https://www.googleapis.com/auth/userinfo.profile "_ 

    "https://www.googleapis.com/auth/drive.metadata.readonly "_ 

    "https://www.googleapis.com/auth/calendar.readonly" 


  set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,scope,.accessToken,.idtoken,.responseProperties,.error) 

  if isAuthorized {  

    // Googleにはイントロスペクションエンドポイントがありませんので、呼び出す必要はありません。イントロスペクションエンドポイントと表示結果については、RFC 7662を参照してください。   

    w "<h3><span style='color:red;'>GetUserInfo API</span>からのデータ</h3>" 

    // userinfoには専用のAPIがありますが、Get() メソッドを適切なURLで呼び出すだけでも取得できます。     

    try { 

    set tHttpRequest=##class(%Net.HttpRequest).%New() 

      $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).AddAccessToken(tHttpRequest,"query","GOOGLE",..#OAUTH2APPNAME)) 

      $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).GetUserinfo(..#OAUTH2APPNAME,accessToken,,.jsonObject)) 

      w jsonObject.%ToJSON() 

    } catch (e) { 

      w "<h3><span style='color: red;'>エラー: ",$zcvt(e.DisplayString(),"O","HTML")_"</span></h3>"     

    } 


    /****************************************** 

    *                                         * 

    *      他のAPIから情報を取得する      * 

    *                                         * 

    ******************************************/ 

    w "<hr>" 


    do ..RetrieveAPIInfo("/drive/v3/files") 

  

    do ..RetrieveAPIInfo("/calendar/v3/users/me/calendarList") 


  } else { 

    w "<h1>認証されていません!</h1>"   

  } 

  &html<</body> 

  </html>> 

  Quit $$$OK 

} 


  


ClassMethod RetrieveAPIInfo(api As %String) 

{ 

  w "<h3><span style='color:red;'>"_api_"</span>からのデータ</h3><p>" 

  try { 

    set tHttpRequest=##class(%Net.HttpRequest).%New() 

    $$$THROWONERROR(sc,##class(%SYS.OAuth2.AccessToken).AddAccessToken(tHttpRequest,"query","GOOGLE",..#OAUTH2APPNAME)) 

    $$$THROWONERROR(sc,tHttpRequest.Get(..#OAUTH2ROOT_api)) 

    set tHttpResponse=tHttpRequest.HttpResponse 

    s tJSONString=tHttpResponse.Data.Read() 

    if $e(tJSONString)'="{" { 

      // JSONではない 

      d tHttpResponse.OutputToDevice() 

    } else {       

      w tJSONString 

      w "<hr/>" 

      /* 

      // 新しいJSON API 

      &html<<table border=1 style='border-collapse: collapse'>> 

      s tJSONObject={}.%FromJSON(tJSONString) 

      set iterator=tJSONObject.%GetIterator() 

        while iterator.%GetNext(.key,.value) { 

          if $isobject(value) { 

            set iterator1=value.%GetIterator() 

            w "<tr><td>",key,"</td><td><table border=1 style='border-collapse: collapse'>" 

            while iterator1.%GetNext(.key1,.value1) { 

            if $isobject(value1) { 

                set iterator2=value1.%GetIterator() 

                w "<tr><td>",key1,"</td><td><table border=0 style='border-collapse: collapse'>" 

                while iterator2.%GetNext(.key2,.value2) { 

                    write !, "<tr><td>",key2, "</td><td>",value2,"</td></tr>"                    

                } 

                // このようにして埋め込みオブジェクト/配列をどんどん進めていきます 

              w "</table></td></tr>" 

            } else { 

                  write !, "<tr><td>",key1, "</td><td>",value1,"</td></tr>"        

            } 

            } 

          w "</table></td></tr>" 

          } else { 

              write !, "<tr><td>",key, "</td><td>",value,"</td></tr>" 

          } 

        }        

    &html<</table><hr/> 

    > 

    */ 

    } 

  } catch (e) { 

    w "<h3><span style='color: red;'>エラー: ",$zcvt(e.DisplayString(),"O","HTML")_"</span></h3>" 

  } 

} 


} 

  

コードを簡単に見てみましょう。 

1.      まず、有効なアクセストークンがあるかどうかを確認する必要があります(そのため、認証を受けました)。 

2.      トークンがある場合はGoogleが提供し、発行されたアクセストークンがカバーするAPIにリクエストを発行できます。 

3.       そのためには標準の %Net.HttpRequest クラスを使用しますが、呼び出されたAPIの仕様に従ってGETメソッドまたはPOSTメソッドにアクセストークンを追加します。 

4.       ご覧のように、OAUTHフレームワークには GetUserInfo() メソッドが実装されていますが、RetrieveAPIInfo() ヘルパーメソッドの場合と同様に、Google APIの仕様を利用して直接ユーザー情報を取得できます。 

5.       OAUTHの世界ではJSON形式でデータを交換するのが一般的であるため、ここでは受信データを読み取り、それを単にブラウザに出力してます。 受信データを解析して整形し、それをユーザーが理解できる形で表示できるようにするのはアプリケーション開発者の責任です。 しかし、それはこのデモの範囲を超えています。 (ただし、いくつかのコメントアウトされたコードで構文解析のやり方を示しています。) 

以下は、未加工のJSONデータが表示された出力のスクリーンショットです。 

パート2に進んでください。そこでは、認証サーバーおよびOpenID Connectプロバイダーとして機能するInterSystems IRISについて説明します。 

[1]https://tools.ietf.org/html/rfc6749https://tools.ietf.org/html/rfc6750 

0
0 857