#アクセス制御

0 フォロワー · 6 投稿

このタグは、役割 (SQL テーブルに対する 1 件以上のアクセス権限が格納されたコンテナ)、ユーザー (データベース接続時のログイン名の識別子)、および承認 (リソースに対するアクセス権/権限を決める機能) に関連するすべての投稿をまとめます。役割、ユーザー、承認に関する詳細は、ドキュメントを参照してください。

記事 Toshihiko Minamoto · 5月 14, 2024 8m read

「30 秒も経ってるのにサービスを受けられないなんて、 あり得ない! もう結構!」

「大変申し訳ございません。 次回からはご予約なさってはいかがでしょうか。」

お気に入りのレストランでこんなコメントを聞いたら、そんな発言はばかばかしいと思うのではないでしょうか。 でも、API のコンテキストでは、まったく合理的な意見です。 お気に入りのレストランと同じように、API にも常連客がいます。よく訪問するユーザーのことです。 同じように繰り返し予約が可能であればよいと思いませんか?

これには、IRIS の基本機能がいくつか関わってきます。 まず、%SYSTEM.License インターフェースを理解する必要があります。 これは、IRIS インスタンスのライセンス使用状況に関する情報を取得するために提供されているインターフェースです。 次に、%CSP.SessionEvents クラスについて学ぶ必要があります。 このクラスを使うと、CSP セッションのライフサイクル全体で呼び出される様々なメソッドをオーバーライドできます。

まずは、%SYSTEM.License インターフェースから見ていきましょう。 このクラスには、$SYSTEM.License を使って呼び出せるメソッドもあれば、##class(%SYSTEM.License) で呼び出す必要のあるメソッドもあります。 一貫性を維持するためにも、ここでは、すべてのメソッドに使用できる ##class(%SYSTEM.License) を使用しましょう。 このユースケースでは、使用できるライセンス数と特定のユーザーが使用しているライセンス数をチェックできる必要があります。 

まず、使用できるライセンス数を確認しましょう。 少なくとも 1 つのライセンスが使用可能である場合、予約には十分であるため、それ以上に確認する必要はありません。 また、ログインしようとしているユーザーが予約を保持しているユーザーであるかを確認する必要もあります。 これは、以下のように、LUAvailable メソッドへの単純な呼び出しで実現できます。

if (##class(%SYSTEM.License).LUAvailable()) || ($USERNAME = username) > 0{    return $$$OK}

次に、予約済みのユーザーが現在ログイン中であるかを確認する必要があります。 すでにレストランで着席している場合は、使用できる席があるかどうかを確認する必要はありません。 ユーザーが常に特定のアドレスから接続する場合は、LicenseCount メソッドを使用してそれを確認することができます。 ユーザー識別子を引数として取り、そのユーザーがその時点でライセンスを使用しているかどうかを示す整数を返します。 例:

set licenseusage = ##class(%SYSTEM.License).LicenseCount("David@127.0.0.1")

このユーザーがライセンスを使用しているかどうかがわかります。 0 を返す場合、そのユーザーはライセンスを使用していませんが、1 を返す場合はライセンスを使用していることになります。 このメソッドが 1 よりも大きい数値を返す場合、ユーザーが最大接続数を超過しており、接続ごとに 1 つのライセンスを使用していることを意味するため、注意が必要です。 その場合は、別の問題を新たに修正する必要があります!

どのアドレスから接続されているのかがわからない場合は、この状況に別の方法で対応して、いくつかの追加ステップが必要となります。 ここでは、ConnectionList クラスのクエリを使用しましょう。 次のコードを検討してください。

ClassMethod CheckReservation(username As %String) As %Status{    try{        if (##class(%SYSTEM.License).LUAvailable() > 1) || ($USERNAME = username) {            return $$$OK        }        set rs = ##class(%ResultSet).%New("%SYSTEM.License:ConnectionList")        set sc = rs.%Execute()        if $$$ISERR(sc) {$$$ThrowStatus(sc)}        while rs.%Next(){            if $P(rs.%GetData(1),"@",1) = username{                return $$$OK            }        }        $$$ThrowStatus($$$ERROR(5001,"Reserved User Not Logged In."))    }    catch ex{        do ex.Log()        return ex.AsStatus()    }}

このメソッドでは、ユーザー名に基づいて 1 つのライセンスのみを予約できることに注意してください。 ユースケースで 2 つ以上のライセンスを確保する必要がある場合は、ConnectionList クエリの結果を確認し、何人の顧客がログイン済みであり、席を待つ間にバーで最初のグラスを傾けているかを数える必要があります。 次に、残りの LUAvailable が単に 2 以上であるだけでなく、すべての予約に対応できるかを確認する必要があります。

このメソッドは通常、%ResultSet オブジェクトを使用して、%SYSTEM.License クラスから LicenseCount クエリにアクセスします。 一般に %SQL.Statement を使用する方が好ましくはありますが、%PrepareClassQuery は必ずこのクエリで失敗します。 そのため、ここではこれを使用することはできません。 このクエリは各接続に対して行を返すため、その後で結果セットを反復しましょう。 各行に対し、最初の列には、ユーザー名と IP アドレスを@ で区切ったライセンス ID が含まれます。 探しているのは(ユーザー名と IP アドレスの組み合わせではなく)ユーザー名のみであるため、その列の最初の部分を提供されたユーザー名と比較して、一致する場合は成功ステータスを返します。 ただし、結果セット全体にユーザー名が見つからない場合は、エラーを返します。

このメソッドを、代わりにロールをチェックするように変更したい場合(スーパーユーザーがいつでもシステムにアクセスできるように、%All ロールを持つ少なくとも 1 人のユーザーがログインできるようにするなど)、%SYS ネームスペースに切り替えて、ループを反復する過程で各ユーザーのロールを検証することで実現できます。 ##class(Security.Users).Get($P(rs.%GetData(1),”@”,1),.props) を使用してから、props(“Roles”) [ “%All” を使用して、ロールが props(“Roles”) に含まれているかを確認できます。 正しいアプリケーションネームスペースにログインできるように、最終的には元のネームスペースに忘れずに戻るようにしましょう。 すると、コードは以下のようになります。

ClassMethod CheckReservation(role As %String) As %Status{    try{        set returnns = $NAMESPACE        zn "%SYS"        set sc = ##class(Security.Users).Get($USERNAME,.props)        if (##class(%SYSTEM.License).LUAvailable() > 1) || (props("Roles") [ role) {            zn returnns            return $$$OK        }        set rs = ##class(%ResultSet).%New("%SYSTEM.License:ConnectionList")        set sc = rs.%Execute()        if $$$ISERR(sc) {$$$ThrowStatus(sc)}        while rs.%Next(){            set sc = ##class(Security.Users).Get($P(rs.%GetData(1),"@",1),.props)            if props("Roles") [ role{                zn returnns                return $$$OK            }        }        zn returnns        $$$ThrowStatus($$$ERROR(5001,"Reserved User Not Logged In."))    }    catch ex{        do ex.Log()        return ex.AsStatus()    }}

License API 内には、使用できる実用的なクラスクエリが他にもあります。 特定のアプリケーションへのログインを検証する場合は、ConnectionAppList の方が適しているかもしれません。 ライセンスのタイプ(User、CSP、Mixed、または Grace)を検査する場合は、UserList クラスクエリを使用すると良いでしょう。 ProcessList クエリを操作すると、ユーザーが実行するプロセスの詳細をさらに詳しく把握することができます。 ただし、あまりにも夢中になりすぎた場合は、ログインプロセスにゆっくりと取り組むようにしましょう。

この部分のコードが完成したので、どこに配置するかを決めなければなりません。 ただし、きちんと配置するには、%CSP.SessionEvents クラスの基本を知っておく必要があります。 このクラスには、CSP セッションが開始、終了、またはタイムアウトになった場合やユーザーがログインまたはログアウトした場合の動作を制御するためにオーバーライドできるメソッドがいくつか含まれています。 ここでは、セッションに割り当てられたライセンスの前に実行する OnStartSession() メソッドに特に注目できます。 そこでメソッドを実行し、エラーを返した場合には、%session.EndSession を 1 にセットするだけで完了です。 すると、ユーザーには標準のライセンス数不足メッセージが表示され、ログインできないようにすることができます。

%CSP.SessionEvents 拡張機能と以前に定義したクラスメソッドを使って、User.SessionEvents というメソッドを作成します。 次に、その中の 1 つのメソッドのみをオーバーライドし、他の予約ようにスペースを空けておくようにします。 すると、そのメソッドは次のようになります。

ClassMethod OnStartSession(){    if $$$ISERR(##class(User.SessionEvents).CheckReservation("David")){        set %session.EndSession=1    }    Quit $$$OK}

ネームスペースが複数ある場合は、このクラスを使用するすべての CSP アプリケーションにクラスを作成するようにしてください。

最後に、アプリケーションがこれらのセッションを使用できるように構成します。 これは、管理ポータルで、システム管理 > セキュリティ > アプリケーション > Web アプリケーションに移動して行えます。 カスタムセッションイベントクラスを使用するアプリケーションをクリックし、 次に、「イベントクラス」ボックスに User.SessionEvents を入力します。 それ以降、ログインするたびに、カスタマイズされた OnStartSession メソッドが呼び出され、予約の余地がない場合には他のユーザーがログインできなくなります。

ここには、検討すべき重要な実装の詳細が 1 つあります。 これらのセッションイベントをシステム管理ポータルに適用すべきかどうかです。 適用する場合は、何らかの問題が生じても、ログインして修正できない状況に陥る可能性があります。 適用しない場合は、誰かがログインし、予約しようとしていたライセンスを使用してしまう可能性があります。 おそらく、そのユーザーに予約チェックを迂回できる特定のロールがあるかを確認するようにコードを編集することができますが、 これについては、あなた次第です。

今後は、常連客が定期的に訪れるたびに、席に座れることを保証できます!

0
1 100
記事 Toshihiko Minamoto · 4月 18, 2023 14m read

今回は、「IRIS for Health Contest」に応募するために、どのような技術を使ってアプリケーションを開発していたのか、その詳細を紹介したいと思います。

  • OpenAPI仕様からのREST API生成
  • APIとWebページを保護する役割ベースのアクセス(RBAC)
  • InterSystems FHIR サーバー

内容

アプリケーション概要

まず、それらの技術に支えられたアプリケーションを簡単に紹介します。

このアプリケーションは、妊娠中の女性が簡単に症状を報告できるように設計されています。このアプリケーションはレスポンシブであるため、モバイルデバイスを使用して症状を簡単に報告することができます。このような症状は、FHIR Observation リソースInterSystems FHIR サーバーを使って記録されます。.

患者と医師は、通常のリレーショナル・テーブルを使用し、患者医師 のFHIRリソースのIDを参照して連携しています。そのため、医師は患者がどのような症状を訴えているのかを確認することもでき、万が一の事態に迅速に対応することができます。

アプリケーションは、IRIS リソース役割を使用して患者/医師を識別し、アクセス権限を制御します。

FHIRリソースは、アプリケーションのフロントエンドで利用可能なREST APIによってアクセスされます。IRIS Interoperability Credentialsに格納されたAPI KEYを使用して、FHIRサーバーへのHTTPS接続が確立されます。

アプリケーションのWebリソースは、IRIS Web Gatewayで扱われます。

一般的なアプリケーションのアーキテクチャ

OpenAPI仕様からのREST API生成

IRISプラットフォームでは、手動またはOpenAPI仕様を介してRESTインターフェースを定義することができます。

OpenAPIを使用することは、デザインファーストのアプローチ、容易な変更追跡、容易な文書化、強力なモデル、設計、モッキング、テストなどのための多くのツールなど、多くの利点があります。

そこで、IRIS REST Servicesを使って、OpenAPI仕様からコードを生成する方法に焦点を当てます。

まず、OpenAPIを使ってAPIを設計する必要があります。今回は、VS Codeの拡張機能であるOpenAPI (Swagger) Editorを使用しました。これは、エンドポイントやその他のOpenAPIリソースをVS Codeで直接作成するのに役立ちます。

VS Code用OpenAPI拡張子

OpenAPI仕様でAPIを設計したら、それをJSONファイルに保存する必要があります。ここでは、このファイルにAPIを保存しています。

これで、IRIS REST Services を使用して API 用のコードを生成する準備が整いました。これには3つのオプションがあります。

この記事では、最後の1つである %REST.API クラスを使用することにします。それでは、IRIS端末を開いて、以下のコードを実行してください。

Set applicationName = "dc.apps.pregsymptracker.restapi"
Set swagger = "/irisrun/repo/src/openapi/pregnancy-symptoms-tracker.json"
ZW ##class(%REST.API).CreateApplication(applicationName, swagger, , .newApplication, .internalError)
ZW newApplication
ZW internalError

OpenApi仕様のJSONファイルの場所は、swaggerパラメータで設定されます。

applicationName パラメータは、IRIS REST Servicesが生成されたコードを格納するパッケージ名です。

REST APIクラスを生成する

3つのクラスが生成されます。

  • spec.cls: OpenAPI仕様のための単なるコンテナです。このクラスは編集しないでください。

  • impl.cls: メソッドの実装を含むメインクラスです。このクラスは、APIロジックを開発するために、自分で編集することを意図しています。ヒント: OpenAPIのメソッドの名前は、必ずIRIS拡張属性 operationId を使って、 ここで のように定義します。この属性を使用しない場合、IRIS はランダムな名前のメソッドを作成します。

  • disp.cls: ディスパッチクラスは、IRIS で REST API を公開するために Web アプリケーションをバインドするクラスです。ヒント: このクラスを表示するには、生成されたアイテムを表示していることを確認します。このクラスを編集することもできますが、あまりお勧めできませんが、IRIS に任せてください。

REST API ディスパッチクラス

最後の2つのパラメータ、 newApplicationinternalError は出力パラメータで、それぞれ API が作成または更新されたかどうか、そして OpenAPI のパースやクラスの生成時に発生した可能性のあるエラーを返すものです。この情報をチェックするために書き出すだけです。

OpenAPI の仕様を更新した場合、コードを更新するために CreateApplication メソッドを再度実行する必要があります。 impl クラスに実装した以前のロジックコードはそのまま残し、IRIS REST Service が修正を行った箇所にはコメントを追加します。

APIとWebページを保護する役割ベースのアクセス(RBAC)

前述したように、このアプリケーションには、患者と医師の2種類のユーザーが存在します。そこで、この2種類のユーザー間でアプリケーションのリソースに対するアクセスルールを設計するために、リソースと役割を使用しました。

ユーザーは役割を与えられ、役割にはリソースへの権限があり、リソースは例えばREST APIのようなシステムリソースにアクセスするために必要であるべきです。

REST APIの安全性確保

IRIS REST Serviceでは、OpenAPIのIRIS拡張子である x-ISC_RequiredResource 属性によって、サービスにアクセスするために必要な権限を指定することができました。この属性は、API全体、または特定のエンドポイントに対して、次のように指定することができます:

    "paths": {
        "/symptom": {
            "post": {
                "operationId": "PostSymptom",
                "x-ISC_RequiredResource": ["AppSymptoms:write"],
                "description": "患者さんが自分の症状を報告するために使用する",
        …
        "/doctor/patients": {
            "get": {
                "operationId": "GetDoctorPatientsList",
                "x-ISC_RequiredResource": ["AppAccessDoctorPatients:read"],
                "description": "現在ログインしている医師の患者を取得する",
        …

OpenAPI仕様でAPIクラスを生成した後 - 前に説明したように , IRIS REST Serviceがx-ISC_RequiredResource制約をdispクラスでどのように実装しているかを見ることができます:

ClassMethod PostSymptom() As %Status
{
    Try {
        Set authorized=0
        Do {
            If '$system.Security.Check("AppSymptoms","write") Quit
            Set authorized=1
        } While 0
        If 'authorized Do ##class(%REST.Impl).%ReportRESTError(..#HTTP403FORBIDDEN,$$$ERROR($$$RESTResource)) Quit
        …
    } Catch (ex) {
        Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("dc.apps.pregsymptracker.restapi.impl","ExposeServerExceptions"))
    }
    Quit $$$OK
}

RBACを使ったAPIの保護方法については、このページをご覧ください。

Webページの安全性確保

このアプリケーションでは、Web アプリケーションを実装するために CSP ページを使用しました。この技術は、現在の SPA に比べて古いと考えられていますが、それでもまだ利点があります。

例えば、ユーザーがページにアクセスするために、特定の役割を持つことを要求することができます。つまり、REST API のエンドポイントを保護することをまとめると、先に述べたように、アプリケーションに追加のセキュリティ・レイヤーを定義することができるのです。

ユーザがシステムにログインするとき、そのユーザに役割が割り当てられている場合、IRISはそのユーザに役割を割り当てます。このような役割は、CSP コンテキストで $ROLE コンテキスト変数を通してアクセスすることができ、ユーザに割り当てられた特定の役割を要求するために使用することができます。

<!-- patient.csp -->
<script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppPatient")
Return 1
</script>
<!-- doctor.csp -->
<script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppDoctor")
Return 1
</script>
ClassMethod AssertRole(pRole As %String)
{
    If ('$Find($ROLES, pRole)){
        Set %response.Redirect = "NoPrivilegesPage.csp"
    }
}

もし、現在のユーザーが patient.csp ページを評価するときに AppPatient 役割を持っていない場合、IRIS Web サーバはそのユーザーを NoPrivilegesPage.csp ページにリダイレクトし、ユーザーにセキュリティ問題を通知するメッセージを表示します。doctor.cpsページも同様ですが、今度は AppDoctor` 役割が必要です。

この例では、AppPatientAppDoctorの2つの役割を持つことができます。つまり、そのユーザーは患者であると同時に医師でもあり、両方のページにアクセスすることができるのです。

リソースと役割の作成

IRISポータルでリソース, 役割 を作成し、ユーザー に割り当てることができます - これは簡単なことです。しかし、ここではプログラムでそれらを作成する方法を紹介したいと思います:

ClassMethod CreateResources()
{
    Do ..Log("アプリケーションリソースを作成する...")
    Set ns = $NAMESPACE
    Try {
        ZN "%SYS"
        Do $CLASSMETHOD("Security.Resources", "Delete", "AppSymptoms")
        Do $CLASSMETHOD("Security.Resources", "Delete", "AppAccessDoctorPatients")
        $$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppSymptoms", "患者の症状", "RWU", ""))
        $$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppAccessDoctorPatients", "患者のアクセス権", "RWU", ""))
    } Catch(e) {
        ZN ns
        Throw e
    }
    ZN ns
}

ClassMethod CreateRoles()
{
    Do ..Log("アプリケーション役割を作成する...")
    Set ns = $NAMESPACE
    Try {
        ZN "%SYS"
        Do $CLASSMETHOD("Security.Roles", "Delete", "AppPatient")
        Do $CLASSMETHOD("Security.Roles", "Delete", "AppDoctor")
        $$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppPatient", "アプリケーション での患者の役割", "AppSymptoms:RWU", ""))
        $$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppDoctor", "アプリケーション での医師の役割", "AppSymptoms:RWU,AppAccessDoctorPatients:RWU", ""))
    } Catch(e) {
        ZN ns
        Throw e
    }
    ZN ns
}

ClassMethod CreateUsers()
{
    Do ##class(dc.apps.pregsymptracker.util.Setup).Log("サンプルユーザーを作成する...")

    //ある患者
    &SQL(drop user MarySmith)
    &SQL(create user MarySmith identified by 'marysmith')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to MarySmith)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to MarySmith)

    //他患者
    &SQL(drop user SuzieMartinez)
    &SQL(create user SuzieMartinez identified by 'suziemartinez')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to SuzieMartinez)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to SuzieMartinez)

    //ある医師
    &SQL(drop user PeterMorgan)
    &SQL(create user PeterMorgan identified by 'petermorgan')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor to PeterMorgan)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to PeterMorgan)

    // 患者である医師
    &SQL(drop user AnneJackson)
    &SQL(create user AnneJackson identified by 'annejackson')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor, AppPatient to AnneJackson)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to AnneJackson)
}

InterSystems FHIR サーバー

InterSystems FHIR サーバー は、IRIS for Health と同じように、FHIR リソースへのアクセスをクラウド上で提供するサービスです。

FHIR ServerではOAuth2が可能で、SMART on FHIR JavaScript Library などのライブラリを使ってアプリケーションから直接FHIRリソースにアクセスできますが、このアプリケーションでは、FHIR Serverをメインデータリポジトリとして使用しながら、IRISにローカルに保存されているメタデータで制御するハイブリッドアプローチを選択しました。

そこで、バックエンドがFHIR ServerでFHIRトランザクションを実行するために使用するFHIRクライアントを作成しました。このクライアントは、サーバーが生成したAPI KEYを使用して、FHIR ServerへのHTTPSコールを実行するために、%Net.HttpRequestを使用して実装されています。

これはHTTPクライアントのコードで、 `` を使って基本的なHTTP動詞を実装しています。

Class dc.apps.pregsymptracker.restapi.HTTPClient Extends %RegisteredObject
{

Property Request As %Net.HttpRequest;

Property Server As %String;

Property Port As %String;

Property UseHTTPS As %Boolean;

Property SSLConfig As %String;

Property APIKeyCred As %String;

Method CreateRequest()
{
    Set ..Request = ##class(%Net.HttpRequest).%New()
    Set ..Request.Server = ..Server
    Set ..Request.Port = ..Port
    Set ..Request.Https = ..UseHTTPS
    If (..UseHTTPS) {
        Do ..Request.SSLConfigurationSet(..SSLConfig)
    }
}

Method SetHeaders(headers As %DynamicObject)
{
    Set headersIt = headers.%GetIterator()
    While (headersIt.%GetNext(.headerName, .headerValue)) {
        Do ..Request.SetHeader(headerName, headerValue)
    }
}

Method GetApiKeyFromEnsCredentials() As %String
{
    Set apiKeyCred = ..APIKeyCred
    $$$TOE(st, ##class(Ens.Config.Credentials).GetCredentialsObj(.apiKeyCredObj, "", "Ens.Config.Credentials", apiKeyCred))
    Return apiKeyCredObj.Password
}

Method HTTPGet(pPath As %String) As %Net.HttpResponse
{
    Do ..CreateRequest()
    $$$TOE(st, ..Request.Get(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPPost(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
    Do ..CreateRequest()
    Do ..Request.EntityBody.Clear()
    Do ..Request.EntityBody.Write(pBody.%ToJSON())
    $$$TOE(st, ..Request.Post(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPPut(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
    Do ..CreateRequest()
    Do ..Request.EntityBody.Clear()
    Do ..Request.EntityBody.Write(pBody.%ToJSON())
    $$$TOE(st, ..Request.Put(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPDelete(pPath As %String) As %Net.HttpResponse
{
    Do ..CreateRequest()
    $$$TOE(st, ..Request.Delete(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

}

そしてこれがFHIRクライアントのコードで、HTTPクライアントを拡張し、CreateRequestメソッドをオーバーライドしてHTTPコールにFHIRサーバーのAPIキーを自動的に付加しています。

Class dc.apps.pregsymptracker.restapi.FHIRaaSClient Extends dc.apps.pregsymptracker.restapi.HTTPClient
{

Method CreateRequest()
{
    Do ##super()
    Do ..SetHeaders({
        "x-api-key" : (..GetApiKeyFromEnsCredentials())
    })
}

}
0
0 149
記事 Toshihiko Minamoto · 12月 7, 2020 10m read

    以前の記事では Arduino を使い始め、最終的には気象観測所のデータを表示できるようになりました。 この記事ではさらに掘り下げ、InterSystems Caché アプリケーションに対して RFID カードと Arduino を介した認証をセットアップします。

 

認証の委任

Caché には認証コードの書き込みを許可することで、認証を委任するための仕組みがあります。 この仕組みを有効にするには、次の手順を実行する必要があります。

  • ZAUTHENTICATE ルーチンにユーザー認証コードを記述します。 このルーチンにはユーザー名/パスワードの取得、それらの検証と権限の割り当て、パスワード変更、トークン生成の 4 つのエントリポイントがあります。 詳細については、以下をお読みください。
    1. Caché で委任認証を有効にします([SMP] → [System Administration] → [Security] → [System Security] → [Authentication/CSP Session Options] を開き、[Allow Delegated authentication] ボックスにチェックを入れて設定を保存します)。
    2. 関連するサービスかアプリケーションの委任認証を有効にします(前者の場合は [SMP] → [Menu] → [Manage Services] → [Service] → [Allowed Authentication Methods] → [Delegated] を選択 → [Save]、後者の場合は [SMP] → [Menu] → [Manage Web Applications] → [Application] → [Allowed Authentication Methods] → [Delegated] を選択 → [Save])。

    仕組み

    委任認証は、委任認証が有効になっているサービスや Web アプリケーションに対してユーザーが認証される際に発生します。

    1. ZAUTHENTICATE ルーチンが呼び出されます。 このルーチンのコードはユーザーによって書かれたものであり、OS への呼び出しを含む任意の Caché ObjectScript コードである可能性があります。
    2. 次のステップは、ZAUTHENTICATE の呼び出しが成功したかどうかによって決まります。
    • ZAUTHENTICATE の呼び出しが成功し、ユーザーが ZAUTHENTICATE で認証されたのが初めてだった場合は「委任されたユーザー」が作成されます。 ZAUTHENTICATE がロールやその他の権限をユーザーに割り当てた場合は、それらがユーザープロパティになります。
    • ZAUTHENTICATE の呼び出しが成功し、ユーザーが ZAUTHENTICATE で認証されたのが初めてではなかった場合はそのユーザーのレコードが更新されます。
    • ZAUTHENTICATE の呼び出しが成功しなかった場合、ユーザーはアクセスエラーを受け取ります。
    1. インスタンスとサービスで 2 要素認証が有効になっている場合は、ユーザーの電話番号と事業者の検索が開始されます。 これらの情報が入力されている場合は、2 要素認証が実行されます。 入力されていない場合は、ユーザーは認証されません。

    ユーザー情報の出処は?

    次のように、アプリケーション/サービスで有効になっている認証方法に応じた 2 つの認証方法があります。

    • 委任: ユーザー名/パスワードは ZAUTHENTICATE ルーチン(GetCredentials エントリポイント)から取得され、ZAUTHENTICATE を使用して検証されます(ユーザータイプ: 委任)。
  • 委任およびパスワード: ユーザー名/パスワードは GetCredentials から取得されますが、標準の Caché ツールを使用してチェックされます(ユーザータイプ: Caché)。
  • 次に、ZAUTHENTICATE ルーチンとそのエントリポイントを見てみましょう。

    ZAUTHENTICATE

    これはメインルーチンであり、次の 4 つのエントリポイントで構成されています。

    GetCredentials

    このエントリポイントはサービスで委任認証が有効になっている場合に呼び出され、ユーザーにユーザー名/パスワードの入力を求める代わりに呼び出されます。 このルーチンのコードは、ユーザー名とパスワードを(何らかの方法で)取得します。 その後、(このエントリポイントの外部で)受信したユーザー名とパスワードはユーザーから通常の方法で入力されたかのように認証されます。 ユーザー名とパスワードは、キーボードからの入力、API、外部デバイスを使用したスキャンなど、任意の方法で取得できます。 この記事では、RFID カードを使用した認証を実装します。

    このエントリポイントはステータスを返します。ステータスがエラーの場合は監査ログに記録され、認証試行は拒否されます。 ただし、エラーステータス $SYSTEM.Status.Error($$$GetCredentialsFailed) が返された場合は、例外的に通常のユーザー名/パスワードの入力が続きます。 シグネチャは次のとおりです。

    GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public { }

    説明:

    • ServiceName – 接続が確立されるサービスの名前
    • Namespace – ネームスペース(接続時に指定されている場合)
    • Username – ユーザー名
    • Password – パスワード
  • Credentials – 現在使用されていません
  • このエントリポイントの重要な機能について説明します。 委任認証とパスワード認証の両方がサービスやアプリケーションで有効になっている場合、ユーザー名とパスワードは GetCredentials エントリポイントを介して受信されますが、それらの情報は標準のパスワード認証に使用されます(ユーザーが手動で入力した場合と同じ)。また、認証が成功した場合のユーザーは委任ユーザーではなく通常の Cache ユーザーになります。

    ZAUTHENTICATE

    初回認証が成功すると、ZAUTHENTICATE はロールやその他のユーザープロパティを定義します。 初回認証以外の場合はプロパティが更新されます(例えば、Roles はログインのたびに指定する必要があります)。 そのために、Properties 配列のプロパティを定型化したコードで設定する必要があります。 シグネチャは以下のとおりです。

    ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) Public { }

    Properties 配列の説明:

    • Properties("Comment") — コメント
    • Properties("FullName") — 氏名
    • Properties("NameSpace") — 初期ネームスペース
    • Properties("Roles") — カンマ区切りのロールのリスト
    • Properties("Routine") — 初期ルーチン
    • Properties("Password") — パスワード
    • Properties("Username") — ユーザー名
    • Properties("PhoneNumber") — ユーザーの電話番号
    • Properties("PhoneProvider") — 電話会社
  • Properties("AutheEnabled") — 標準の 2 要素認証を有効化します(この目的のために、$$$AutheTwoFactorSMS に等しい値を設定する必要があります)
  • ChangePassword

    ユーザーパスワードを変更するためのエントリポイントです。シグネチャは次のとおりです。

    ChangePassword(Username, NewPassword, OldPassword, Status) Public { }

    説明:

    • NewPassword — 新しいパスワード
    • OldPassword — 古いパスワード
  • Status — パスワード変更の結果
  • SendTwoFactorToken

    標準の 2 要素認証で使用されるものです。 リクエストの形式と認証トークンを指定します。 シグネチャは以下のとおりです。

    SendTwoFactorToken(Username, ServiceName,Namespace,Application,Credentials,SecurityToken,TwoFactorTimeout,UserPhoneNumber) Public { }
    

    説明:

    • Application — ユーザーが接続している CSP アプリケーションまたはルーチン
    • SecurityToken — ユーザーに送信されるトークン
    • TwoFactorTimeout — トークンの有効期限が切れる時間
  • UserPhoneNumber — ユーザーの電話番号
  • まずは簡単な例から始めましょう。Windows での Caché ターミナルを担う %Service_Console サービスは、ユーザー名とパスワードの入力をユーザーに要求します。 このサービスに対して委任認証を有効にしましょう。 以下は、ユーザーにユーザー名とパスワードの入力を要求する ZAUTHENTICATE ルーチン(%SYS ネームスペース内)です。

    ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC {
        #Include %occStatus
        Quit $$$OK
    }
    GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public {
        #Include %occErrors
        #Include %occStatus
        Do ##class(%Prompt).GetString("USER:",.Username)
        Do ##class(%Prompt).GetString("PASS:",.Password)
        Quit $$$OK
    }

    ターミナルの場合、これは通常のユーザー名認証と同じように見えます。

    >USER: _SYSTEM
    >PASS: SYS

    RFID

    それでは、RFID による認証を見てみましょう。 考え方は単純で、Caché が暗号化されたユーザー名とパスワードをカードに書き込み、認証中に Caché がカードをスキャンして復号化し、受け取ったユーザー名とパスワードを認証に使用するというものです。

    まず、Arduino Uno と RFID-RC522 モジュールの回路図をご覧ください。

    MF522 ライブラリを使用した C のコードはここにあります。 このコードでは、COM ポート経由で次の 2 つのコマンドを受信できます。

    • Get – RFID カードのブロック 2 / 4 / 5 / 6 の内容が COM ポートに渡されます
  • Set@bloc2@bloc4@bloc5@bloc6 — ブロック 2 / 4 / 5 / 6 の値が受信したデータに置き換えられます
  • Caché 側には Arduino.Delegate クラスがあり、その中に次の 2 つの対応するエントリポイントがあります。

    • SetCredentials — ユーザー名とパスワードの入力を取得し、それをシステムに格納されているキーを使用して AES 暗号化で暗号化し、RFID カードに書き込みます。
  • GetCredentials — カードから暗号化テキストを受信して復号化し、ユーザー名、パスワード、および操作のステータスを返します。
  • また、GetCredentials を使用して Arduino.Delegated クラスを呼び出す ZAUTHENTICATE ルーチンは以下のとおりです。

    ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC {
        #Include %occStatus
        Quit $$$OK
    }
    GetCredentials(ServiceName, Namespace, Username, Password, Credentials) Public {
        #Include %occErrors
        #Include %occStatus
        Quit ##class(Arduino.Delegated).GetCredentials(.Username, .Password)
    }
    

    これで準備完了です! 組み立て後のデバイスは次のようになります。

    次のようにターミナルでシステム暗号化キーを設定します(%SYS ネームスペースと Arduino.Delegated クラスを使用できる必要があります)。

    Do ##class(Arduino.Delegated).InitEncryption(Key, IV)
    

    ここで、Key は暗号化キー、IV は初期化ベクトルです。 これらは、ユーザー名とパスワードを暗号化するために使用されます。 コマンドを使用して認証するには、Arduino を Caché に接続し、カードに情報を書き込みます。

    Do ##class(Arduino.Delegated).SetCredentials("_SYSTEM", "SYS")
    

    適切なサービスまたは Web アプリケーション(端末やシステム管理ポータルなど)で委任認証とパスワード認証を有効にすると、カードを RFID カードリーダーにかざすことで認証できるようになります。

    考えられる機能強化

    • マネージド暗号化キーを使用してユーザー名とパスワードを暗号化すると、セキュリティを強化できます。
    • 2 要素認証を使用すると、セキュリティを強化できます。具体的には、先にユーザー名とパスワードのペアを取得してから、ユーザーに固有のキーが格納されているカードを読み取ります。 次に、受信したキーをシステムに格納されている特定ユーザーのキーで確認する必要があります。 任意のユーザーデータを格納する方法は、InterSystems のコミュニティで議論されています
  • それぞれ 15 文字を超えるユーザー名とパスワードを格納する機能を追加します。
  • まとめ

    柔軟性の高い Caché の認証システムを使えば、任意のユーザー認証ロジックを実装できます。

    リンク

    0
    0 1034
    記事 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