投稿者

インターシステムズジャパン
記事 Toshihiko Minamoto · 3月 12 11m read

JWT認証

私が若かった頃(正確にどれほど若かったかという質問はこの記事の範囲外ですが)、「トークン」という言葉は私にとって楽しみそのものでした。 というのも、年に数回だけ、友だちと一緒にゲームセンターに行って、面白いビデオゲームで遊ぶことができたからです。

今では、トークンはセキュリティを象徴するものになりました。 JSON Web Token(JWT)認証は、REST APIをセキュリティで保護するための最も人気の標準の1つになりました。 幸いなことに、IRISでは、この方式でアプリケーションを保護するための設定をシンプルに行える仕組みがあります。 それでも、アイデアは昔のアーケードで遊んでいた頃とどこか似ています。 ゲームで遊ぶには、トークンを手に入れる必要がありますよね!

セットアップ

まず最初に、ユーザーがトークンを取得できるようにIRISを設定します。 まずは、JWT認証をシステム全体で有効にするところから始めます。 これを行うには、システム管理ポータルを開き、サインインします。 次に、「System Administration」 > 「Security」> 「System Security」 >「Authentication\Web Session Options」に移動します。 ここは、LDAPや二要素認証など、他の認証方法を承認する際に使用する同じエリアです。 バリアントのリストの下のほうに「JWT 発行者フィールド」というフィールドがあり、トークンの発行者を特定する値を入力する必要があります。 これは任意の一意な文字列にできますが、URLやドメインが指定されることが多いです。 この値は、事前にAPI開発者とフロントエンド開発者の間で合意しておく必要があります。 APIにアクセスする際にユーザーがリクエストを送信するURLを選択できます。 私の例では、www.myurl.comを選択します。

次に、システム管理ポータルでWebアプリケーションを設定する必要があります。 これを行うには、まず「System Administration」>「Security」>「Applications」> 「Web Applications」に戻ります。 次に、RESTを選択し、通常通りアプリケーションのディスパッチクラスを設定します。ただし今回は、下にある「Use JWT Authentication(JWT認証を使用)」ボックスにチェックを入れる必要があります。 アクセスとリフレッシュのトークン期限を調整することもできますが、通常はデフォルトのままで十分です。 アプリケーションを保存しましょう。 結果は、以下のスクリーンショットのようになっているはずです。

設定後、IRISは自動的にいくつかのエンドポイントをAPIに追加します。 デフォルトでは、エンドポイントは/login、/logout、/refresh、/revokeです。 エンドポイントをカスタマイズしたい場合は、ディスパッチクラス内でパラメーターをいくつか定義できます。 例えば、すべてのエンドポイントの先頭に「/auth」を付ける場合、ディスパッチクラスに以下の設定を追加します。  

Parameter TokenLoginEndpoint = "jwtlogin";
Parameter TokenLogoutEndpoint = "jwtlogout";
Parameter TokenRevokeEndpoint = "jwtrevoke";
Parameter TokenRefreshEndpoint = "jwtrefresh";

これにより、すべてのエンドポイントの名前の前に「jwt」が追加されます。 ただし、今回の目的では、エンドポイントはデフォルト設定のままにしておきます。

ログイン

次に、それらのトークン機能が正しく動作するか確認します。 これらのエンドポイントには、他のAPIエンドポイントと同じようにアクセスできます。 ObjectScriptと%Net.HttpRequestオブジェクトを使用して、ログインエンドポイントにアクセスする例を示します。 IRISからこのAPIにアクセスする場合は、用途に応じて設定をカスタマイズできます。  

Class User.JWTTest Extends %RegisteredObject
{
    ClassMethod getToken(Output tokenobj) As %Status
    {
        try{
            set myreq = ##class(%Net.HttpRequest).%New()
            set myobj = ##class(%Library.DynamicObject).%New()
            set myreq.Server = "localhost"
            set myreq.Location = "/iris/jwtauth/login"
            set myreq.ContentType = "application/json"
            do myobj.%Set("user","APIUser")
            do myobj.%Set("password","mypassword")
            do myobj.%ToJSON(myreq.EntityBody)
            do myreq.Post()
            set tokenobj = ##class(%Library.DynamicObject).%FromJSON(myreq.HttpResponse.Data)
            return $$$OK
        }
        catch ex{
            write ex.DisplayString()
            return ex.AsStatus()
        }
    }
}

簡単なトラブルシューティングのヒントですが、ログインエンドポイントにアクセスしようとして「404 Not Found」の HTTPステータスが返される場合、Webアプリケーションの設定が正しくないか、アクセスしようとしているURLが間違っている可能性があります。 ただし、リクエストのContentTypeをapplication/jsonに設定し忘れた場合や、POSTではなくGETリクエストを使用した場合にも、404エラーが発生することがあります。

この時点で、以下のネームスペースでターミナルセッションを開き、次のコマンドを実行できます。

set sc = ##class(User.JWTTest).getToken(.tokenobj)

これにより、ログインリクエストのレスポンスを含む動的オブジェクトが生成されます。 このオブジェクトを確認すると、いくつかのプロパティがあることがわかります。

access_token アクセストークンの文字列を含んでいます
refresh_token 更新トークンの文字列を含んでいます
sub サブスクライバーは、トークンが誰のためのものかを示します(この場合は、ログインに使用したユーザー名です)。
iat これはトークンが発行された時点のUnixタイムスタンプです。
exp これはトークンの有効期限が切れる時点のUnixタイムスタンプです。

したがって、tokenobj.%Get(“access_token”)を使用してトークンを取得できます。 これでJWTを取得できましたが、そもそもJWTとは何なのでしょう?

JWTトークンの構造

かつての大量生産された偽コインとは異なり、各JSON Web Tokenはそれぞれ固有で独立したものです。 アクセストークンはエンコードされており、その意味不明な見た目からも分かるはずです。 また、トークンはピリオドで区切られた3つの部分に分かれています。 これらのセグメントには、ヘッダー、ペイロード、署名が含まれています。 最初の2つは、簡単にデコードできます。 以下にあるいくつかのコマンドを使って、ターミナルセッションで詳しく見てみましょう。  

Set token = tokenobj.%Get(“acess_token”)
Set tokenheader = $P(token,”.”,1)
Write $SYSTEM.Encryption.Base64Decode(tokenheader)
Set tokenpayload = $P(token,”.”,2)
Write $SYSTEM.Encryption.Base64Decode(tokenpayload)

実行すると、最初の2つの要素は単純にBase64エンコードされたJSONオブジェクトであることが分かります。 ヘッダーには、署名アルゴリズムが含まれるalgフィールドと、トークンの種類が含まれるtypフィールドの2つのフィールドしかありません。 この場合、JWTトークンのみを使用しており、ヘッダーにもその情報が反映されています。 トークン自体には、元のトークンリクエストのレスポンスで確認したものと同じ発行時刻、有効期限、サブスクライバー情報がそのまま保持されています。 また、システム管理ポータル(サインインしたWebアプリケーション)で入力した発行者フィールドと一致する発行者と、セッションIDも含まれています。

トークンの3つ目の部分は、他の2つとは若干異なります。 これは、システム管理ポータル内のJWT署名アルゴリズムに基づいて暗号化された署名です。 この署名は、前の2つのセクションのコンテンツをハッシュ化および暗号化して生成されます。 サーバーはトークンを受け取ると、ヘッダーとペイロードに基づいて署名を再作成できます。 再生成した署名がトークン上の署名と一致しない場合、サーバーはトークンが改ざんされたと判断し、拒否します。

トークンの使用

トークンを取得したので、操作を開始できます! ディスパッチクラスを非常にシンプルにしましょう。  

Class User.JWTAuth Extends %CSP.REST
{
    XData UrlMap [ XMLNamespace = "http://www.intersystems.com" ]
    {
        
            
        
    }
    ClassMethod Test() As %Status
    {
        write "Success!"
        return $$$OK
    }
}

このエンドポイントにアクセスするには、最初に「Bearer: 」を含むAuthorizationヘッダーでリクエストを送信した後でアクセストークンを送信する必要があります。 以下のメソッドを見てみましょう。User.JWTTestと呼ばれるクラスに配置しています。  

ClassMethod getTest(Output myreq) As %Status
{
    try{
        set sc = ##class(User.JWTTest).getToken(.tokenobj)
        set myreq = ##class(%Net.HttpRequest).%New()
        set myobj = ##class(%Library.DynamicObject).%New()
        set myreq.Server = "localhost"
        set myreq.Location = "/iris/jwtauth/test"
        set myreq.Authorization = "Bearer: "_tokenobj.%Get("access_token")
        do myreq.Get()
        return $$$OK
    }
    catch ex{
        return ex.AsStatus()
    }
}

次に、ターミナルセッションでこのメソッドを呼び出しましょう。

set sc = ##class(User.JWTTest).getTest(.myreq)

リクエストオブジェクトとその応答を調べてみましょう。応答を確認すると、HTTPステータスが200 OKであることがわかります。また、応答のデータを出力すると、予想通り「Success!」と表示されます。

更新

最終的に、「続行するにはトークンを挿入してください」という画面が表示されます。 その時点で、次のエンドポイント(/refresh)に対応する必要があります。 タイムアウト設定に注意していれば、アクセストークンに比べて、更新トークンのタイムアウトは常に長くなることに気づいたはずです。 アクセストークンが期限切れになったら、更新トークンを使用して新しいアクセストークンと新しい更新トークンを取得できます。この際、古いトークンは無効になりますが、セッションを失ったり新しいセッションを開始したりすることはありません。 ログインエンドポイントではなくこのエンドポイントを呼び出すと、ログインイベントの監査ログを記録する設定になっている場合に、システム管理ポータルの監査ログにログインが反映されます。 更新は実際のログインではないため、監査ログにはログインとして記録されません。

更新エンドポイントを使用するには、ログイン時と非常に似たリクエストを送信しますが、本文の内容が少し異なります。  

{
    "refresh_token": "(your refresh token goes here",
    "grant_type": "refresh_token"
}

このリクエストを送信すると、ログインリクエストの応答とまったく同じ構造の応答が返ってきます。 常に新しいアクセストークンと更新トークンが含まれています。 この時点で古いトークンは失効するため、更新トークンがまだ期限切れでない場合でも再利用することはできません。

更新トークンが期限切れの場合、このリクエストは認証エラーで失敗し、その場合は再度ログインする必要があります。

ログアウト

すべてのトークンを使い切り、賞品を集めたら、家に帰る時間です。 ここで選択肢として利用できるエンドポイントには、refreshとrevokeの2つがあります。 なぜ2つあるのでしょうか? ウェブアプリケーションを設定していたときに、Group By IDフィールドのことを言い忘れていました。 ただし、現在のドキュメントによると、このフィールドはもう使用すべきではありませんが、古いアプリケーションでは値が設定されている場合があります。 Group By IDを共有するすべてのアプリケーションは、認証も共有するようにプログラムされています。 つまり、どれか1つのアプリケーションにログインまたはログアウトすると、すべてのアプリケーションにログインまたはログアウトすることになります。 group by IDが設定されている場合、logoutエンドポイントはそのgroup by IDを共有するすべてのセッションをログアウトさせます。 group by IDが設定されていない場合、両方のエンドポイントは同じ動作をします(現在のアクセス トークンと関連する更新トークンを無効化します)。 これらのエンドポイントはPOSTリクエストとauthorizationヘッダーの設定を必要とします。 ただし、本文は不要で、応答にも含まれません。 処理が正常に完了すると、HTTPステータスは200 OKになります。

その後、アクセストークンや更新トークンを使用しようとすると、401 UnauthorizedのHTTPステータスが返されます。

この記事がお役に立てたのであれば幸いです。これで、これらのコツを実装できるようになったと思います。 私は今、ドンキーコングがやりたい気分です!