記事 Shintaro Kaminaka · 8月 20, 2020 23m read

InterSystems IRIS Open Authorization Framework(OAuth 2.0)の実装 - パート2

Cache´ OAuth2 プロバイダーに対する認証と認可

このページのデモでは、OAuth2 の認可を使用して Cache´ API 関数を呼び出す方法を示しています。

ここでは Cache´ の認証および認可サーバーを呼び出し、アプリケーションに別の Cache´ サーバーに保存されているデータへのアクセスを許可します。 > // 適切なリダイレクトとスコープを持つ認証エンドポイントの 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))_"
",! } &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<
  
>
    
    // OAuth2 サーバーからのアクセストークンがあるかどうかをチェックします。
    set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1 scope2",.accessToken,.idtoken,.responseProperties,.error)
    
    // アクセストークンがあるかどうかをさらにチェックします。
    // 以下は実行可能なすべてのテストであり、あらゆるケースで必要なわけではありません。
    // 各テストで返される JSON オブジェクトが単に表示されています。
    if isAuthorized {
        write "

Authorized!

",!                           // JWT の場合、検証してからアクセストークンから詳細を取得します。         set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,accessToken,"scope1 scope2",,.jsonObject,.securityParameters,.sc)         if $$$ISOK(sc) {             if valid {                 write "Valid JWT"_"
",!                 } else {                 write "Invalid JWT"_"
",!                 }             write "Access token="             do jsonObject.%ToJSON()             write "
",!         } else {             write "JWT Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!             }         write "
",!         // イントロスペクションエンドポイントを呼び出して結果を表示します。RFC 7662 を参照してください。         set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.jsonObject)         if $$$ISOK(sc) {             write "Introspection="             do jsonObject.%ToJSON()             write "
",!         } else {             write "Introspection Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!             }         write "
",!                  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"_"
",!                     } else {                     write "Invalid IDToken"_"
",!                     }                 write "IDToken="                 do jsonObject.%ToJSON()                 write "
",!             } else {                 write "IDToken Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!                 }         } else {             write "No IDToken returned"_"
",!         }         write "
",!              // アプリケーションロジックには不要ですが、委任認証に渡すことができるユーザーに関する情報を提供します。              // Userinfo エンドポイントを呼び出して結果を表示します。OpenID Connect Core の仕様を参照してください。         set sc=##class(%SYS.OAuth2.AccessToken).GetUserinfo(             ..#OAUTH2APPNAME,             accessToken,,             .jsonObject)         if $$$ISOK(sc) {             write "Userinfo="             do jsonObject.%ToJSON()             write "
",!         } else {             write "Userinfo Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!             }         write "

",!         /***************************************************         *                                                  *         *   リソースサーバーを呼び出し、結果を表示します。   *         *                                                  *         ***************************************************/                          // オプション 1 - リソースサーバー - 定義によれば認可サーバーからのデータを信頼します。         //     そのため、リソースサーバーに渡されたアクセストークンが有効である限り         //  要求元を問わずデータを提供します。                  // オプション 2 - または委任認証(OpenID Connect)を使用して          //  (委任認証を保護して)別の CSP アプリケーションを呼び出すこともできます。         //  - これはまさにこのデモで実施する内容です。                           write "

リソースサーバーの呼び出し(委任認証)","

",!         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,"
",!         }         if $$$ISERR(sc) {             write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!             }         write "
",!              write "

Call resource server - no auth, just token validity check","

",!         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,"
",!         }         if $$$ISERR(sc) {             write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!             }         write "
",!     } else {         write "Not Authorized!

",!         write "Authorize me"     }         &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 "

Hello from Cach&eacute; server: /csp/portfolio2 application!

" w "

running code as $username = "_$username_" with following $roles = "_$roles_" at node "_$p($zu(86),"*",2)_"." } } else { w "

NOT AUTHORIZED!

" w "
"
        w
        i $d(%objlasterror) d $system.OBJ.DisplayError()
        w "
" } 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 "

Hello from Cach&eacute; server: /csp/portfolio application!

" w "

running code as $username = "_$username_" with following $roles = "_$roles_" at node "_$p($zu(86),"*",2)_"." Quit $$$OK } }

そして最後に、/csp/portfolio の Web アプリケーション構成を示します。

あなたが本当に心配であったなら、最初のバリエーションで行ったように _Permitted クラス_を設定できたかもしれません。 または、REST API を使用していたかもしれません。 しかし、これらの設定に関してはこの記事の中では説明しません。

次回は、InterSystems IRIS OAUTH フレームワークによって導入される個々のクラスについて説明します。 それらの API、およびそれらを呼び出すタイミングと場所について説明します。

 

[1] この記事で OAUTH について言及する場合、常に RFC 6749()で規定されている OAuth 2.0 を指しています。 ここでは簡略化のために短縮形の OAUTH を使用しています。

[2] OpenID Connect は OpenID Foundation()によって管理されています。

元の記事へ さんが書いた @Daniel Kutac