RPG開発者向けのWebの基本概念 Part 4
この記事シリーズを締め括る最後の記事は、 JWT (JSON Web Token)についてです。以前の記事のおさらいをしたいという方は、 パート3から読み返すとよいでしょう。無人のM2M(マシン ツー マシン)プロセスでは、JWTストリングは、リソース サーバーにアクセスを要求するためのフォーマットされたコンテナとなります。ここでは、リソース サーバーをアプリケーションとして考えます。また、このケースでは、JWTは非対称署名を使用するとします。
しかし、実際のところ、JWTとはどのようなものでしょうか。それは、一時的な運転免許証のようなものと考えるとよいでしょう。ホテルに泊まった時のことを思い出してみてください(図1)。
ホテルでの手続きの流れ
- ホテルにチェックインするときには、身分証(JWT)を提示します。しかし、JWTは政府機関が発行する身分証ではなく、自分自身が発行する身分証です。そのため、自分で身分証を作成する必要があります。
- ホテルのフロント受付係が、写真付きの身分証が有効かどうかを確認します。
- 有効な場合、フロント係はルーム キー(アクセス トークン)を渡します。
- ルーム キーは午後3時から有効となり、いくつかの部屋へ入れるようになりますが、入れない部屋もあります。たとえば、リネン室は立ち入り禁止です。
- ルーム キーの有効期限が切れると、どの部屋にも入れなくなるため、遅れるのは禁物です。
リソース サーバー上のアプリケーションへアクセスする場合、JWTおよびアクセス トークンは、ホテルの例えと同じような経路を辿ります(図2)。
アプリケーションへのアクセスの手続きの流れ
- クライアントは、JWTを作成し、署名して、認証サーバーに送信します。また、HTTPリクエストには、アクセスしたいリソース(アプリケーション)も含まれています。
- 認証サーバーは、クライアントに許可されている権限に基づいて、JWTおよび要求されたリソースを検証します。
- 有効な場合、アクセス トークンがJSONストリングとして返されます(HTTPステータス200)。このアクセス トークンは、おそらく数分で有効期限切れになります。これまでに、最大1000バイトのサイズのアクセス トークンを目にしたことがありますが、もっと大きくなる可能性もあります。
- HTTPリクエストで、アクセス トークンをリソース サーバーにBearerトークン(持参人トークン、署名なしトークン)として渡します。「Bearer(持参人)」というのは、単に、アクセス トークン(一時的に支給されている「ルーム キー」)を提示するだけということです。
- リソース サーバーのレスポンスには、リクエストの結果が含まれています。
JWTのセクション
JWTは、3つのセクション(ヘッダー、ペイロード、署名)が含まれる、長いテキストのストリングです。それぞれのセクションはピリオドによって区切られています。また、各セクションはBase64としてエンコードされています。JWTを可視化したり、デバッグしたりするためのWebサイトとして、 jwt.ioというサイトが好評を博しています。そのサイトでは、右側にそれら3つのセクションが表示され、左側にはエンコードされたBase64が表示されます(図3)。また、Base64がURLセーフである点にも注目してください。すべての「+/」文字がそれぞれ「-_」に変換され、末尾の等号が各セクションからトリミングされています。少し時間を取って、jwt.ioサイトを利用してみて、使い方に慣れておくとよいと思います。では、コード例でセクションごとに詳しく見てみましょう。
ヘッダー セクション
JWTのヘッダーは、署名を生成するのに使用されるアルゴリズムを記述し、 タイプ は「JWT」です。sha-256メッセージ ダイジェストと組み合わせた RSA 秘密鍵は、JWTアルゴリズム「RS256」に相当します。以下のコード スニペットでは、 JSON_OBJECT スカラー関数を使用して、JWTヘッダーをJSONフォーマットで構築してから、 BASE64_ENCODE スカラー関数を使用してそれをBase64に変換します。
01 dcl-s JwtHdrAlg varchar(10) Inz('RS256');
02 dcl-s JwtHdrType varchar(10) Inz('JWT');
03 dcl-s JwtHdrJson varchar(100);
04 dcl-s wkAscii varChar(500) ccsid(*UTF8);
05 dcl-s wkAsciiBase64 varChar(700) ccsid(*UTF8);
06 dcl-s JwtHdrBase64 varchar(150);
07 Exec SQL Set Option
Commit = *NONE, Naming = *SYS,
DLYPRP = *YES, CLOSQLCSR = *ENDACTGRP,
DATFMT = *ISO, TIMFMT = *HMS,
USRPRF = *OWNER, DYNUSRPRF = *OWNER;
08 Exec SQL
values(JSON_OBJECT('alg': :JwtHdrAlg,'typ': :JwtHdrType))
into :JwtHdrJson;
09 wkAscii = JwtHdrJson;
10 exec sql set :wkAsciiBase64 = BASE64_ENCODE(:wkAscii);
11 JwtHdrBase64 = wkAsciiBase64;
12 JwtHdrBase64 = %xlate('+/':'-_':JwtHdrBase64);
13 JwtHdrBase64 = %trimr(JwtHdrBase64:'=');
01行-03行:アルゴリズム、JWTタイプ、およびJSONヘッダーを格納する変数。
04行―06行:JSONヘッダーをBase64へ変換するための変数。ASCIIで終わるようにするためには、ASCIIで始める必要があることを思い出してください( パート1を参照)。ストリングをBase64にエンコードすることは、ビットをシフトし、グループ化するだけです。
07行:私が使用する標準的なSQLプリコンパイルオプションです。
08行:JSONヘッダー ストリングを構築して変数JwtHdrJsonに格納します。ここでJSON_OBJECT関数によって返されるストリングは、「{"alg":"RS256″,"typ":"JWT"}」です。
09行:JSON EBCDICをASCIIに変換します。
10行:JSONをBase64に変換します
11行:ASCIIをEBCDICに変換します。
12行―13行:一部の文字をURLセーフな文字に変換し、また、末尾の等号をすべてトリミングします。最終的なBase64ヘッダーは、「eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9」という値になります。
デバッグの目的で、コードで生成したJWTを、 jwt.io サイトによって生成されるJWTと照合してみましょう。そのサイトへ移動して、RS256アルゴリズムを選択します(図4)。
「Encoded」入力フィールドで、Base64ヘッダーの値を確認できます(図5)。
しかし、jwt.ioのJSONヘッダーをBase64 エンコーダーにペーストしてみると、結果(図6)はjwt.ioの値と一致しません。
どうしてそうなるのでしょうか。これは、jwt.ioは、実際は、JSONをBase64にエンコードする前に改行およびインデントを除去することによってJSONストリングをフラット化するからです。これは、実はありがたいことです。なぜなら、JSON_OBJECTスカラー関数で生成されるJSONは、jwt.ioがJSONからBase64にエンコードするのに使用するJSONに一致するからです(図5と図7を参照)。これにより、行う必要が生じるかもしれない一部のデバッグ作業が簡略化される場合があります。
ペイロード セクション
JWTのペイロードには、 クレーム が含まれています。クレームは、簡単に言えば、データです。クレームには、標準クレームもあれば、ビジネス ニーズに特有のクレームなどもあります。ここでは、最もよく使用される標準クレームについて説明します。ヘッダーと同様に、ペイロードのJSONは、Base64に変換する必要があります。以下のコード スニペットは、前述のヘッダーのコードをベースにしています。
01 dcl-s JwtPayLdAud varchar(100) Inz('https://my.resource.com/sales');
02 dcl-s JwtPayLdIss varchar(50) Inz('0oabcdefg123456dRTvR');
03 dcl-s JwtPayLdSub varchar(50) Inz('0oabcdefg123456dRTvR');
04 dcl-s JwtPayLdIat packed(10:0) Inz(1726361713);
05 dcl-s JwtPayLdExp packed(10:0);
06 dcl-s JwtPayLdJson varchar(500);
07 dcl-s JwtPayLdBase64 varchar(700);
08 JwtPayLdExp = JwtPayLdIat + 600; // 10 minutes
09 Exec SQL
values(JSON_OBJECT('aud' : :JwtPayLdAud,
'iss' : :JwtPayLdIss,
'sub' : :JwtPayLdSub,
'iat' : :JwtPayLdIat,
'exp' : :JwtPayLdExp))
into :JwtPayLdJson;
10 wkAscii = JwtPayLdJson;
11 Exec SQL set :wkAsciiBase64 = BASE64_ENCODE(:wkAscii);
12 JwtPayLdBase64 = wkAsciiBase64;
13 JwtPayLdBase64 = %xlate('+/':'-_':JwtPayLdBase64);
14 JwtPayLdBase64 = %trimr(JwtPayLdBase64:'=');
01行:「aud」(audience:対象)は、リソース サーバー上でアクセスすることを要求しているURLです。
02行-03行:「iss」(issuer:発行者)および「sub」(subject:主体)は、認証サーバー上でのクライアントの一意のクライアントIDです。このIDは、認証サーバー上でクライアントの公開鍵にマッピングされ、署名を検証するのに使用されます。
04行:「iat」は、「issued at(発行時刻)」を意味します。これは、このJWTが作成された時点のエポック秒数です( パート2を参照)。
05行:「exp」(expiration:有効期限)は、JWTの有効期限が切れる時刻です。「iat」を基準とします。
06行-07行:JWTのJSONのペイロードおよびBase64値を格納する変数。
08行:このJWTは、iatの600秒後に有効期限が切れます。
09行:JWTのJSONのペイロード ストリングを構築して、変数JwtPayLdJsonに格納します。ここでJSON_OBJECT関数によって返されたストリングは、以下の通りです。
'{"aud":"https://my.resource.com/sales","iss":"0oabcdefg123456dRTvR",
"sub":"0oabcdefg123456dRTvR","iat":1726361713,"exp":1726362313}'.
10行-14行:ペイロードをBase64に変換します。おそらくお気付きかと思われますが、ストリングをBase64に変換するためにここでコードを繰り返しています。したがって、このコード ブロックはサブプロシージャーに移動するのにふさわしい候補となるでしょう。
署名セクション
JWTの最後の3番目のセクションは署名です。 パート3 では、IFSストリーム ファイルの署名を生成する方法について取り上げましたが、この例とは、要件が少し異なります。ここでは、ストリングから署名を生成したいので、QShellコマンドの修正が必要です。
01 dcl-s wkTempFile varchar(100);
02 dcl-s QShellCmd varchar(2000);
03 dcl-s JwtSignature varchar(2000);
04 dcl-s Jwt varchar(3000);
05 dcl-pr QCMDEXC extpgm;
Cmd char(2000) options(*varsize) const;
CmdLen packed(15:5) const;
end-pr;
06 Exec SQL
values(hex(generate_unique())) into :wkTempFile;
07 QShellCmd = 'cd /tmp && printf "%s" ' +
08 '"' + JwtHdrBase64 + '.' + JwtPayLdBase64 + '" ' +
09 '| openssl dgst -sha256 -binary -sign ' +
10 '"my_private_key.pem" ' +
11 '-out "' + wkTempFile + '" && ' +
12 'openssl enc -base64 -A -in "' + wkTempFile + '" ' +
13 '| tr -d ''''\n='''' | tr ''''+/'''' ''''-_'''' > "' +
14 wkTempFile + '"';
15 QShellCmd = 'STRQSH CMD(''' + QShellCmd + ''')';
16 CallP(e) QCMDEXC(QShellCmd:%Len(QShellCmd));
17 wkTempFile = '/tmp/' + wkTempFile;
18 Exec SQL
Select LINE Into :JwtSignature
From Table(QSYS2.IFS_READ(PATH_NAME => : wkTempFile,
END_OF_LINE => 'ANY',
MAXIMUM_LINE_LENGTH => 2000))
Limit 1;
19 Jwt = JwtHdrBase64 + '.' + JwtPayLdBase64 + '.' + JwtSignature;
01行:一意のIFSストリーム ファイル名を格納します。
02行:ここでは、QShellコマンドを使用して署名を生成します。
03行:JWTの署名(Base64)。
04行:最終的なJWTの値。
05行:QShellコマンドを実行するためのQCMDEXCのプロトタイプ。
06行:QShellコマンドは、JWT署名をIFSストリーム ファイルへ書き込みます。同時に実行しているジョブが互いの邪魔にならないよう、ファイル名を一意にするために、 GENERATE_UNIQUE スカラー関数は長さ26文字の一意のストリングを返します。例:0780129106AC5B7743B4A10001。
07行-14行:ここで、QShellコマンドを構築します。これは、 パート3の例と似ています。
07行-08行:QShellは、現行ディレクトリー(CD)で非修飾ファイル名を探します。ここでは、CDは/tmpです。&&は、その前のコマンドが成功した場合にのみ、その次のコマンドを実行することを意味します。次いで、ヘッダーとペイロード(単一のピリオドによって区切られます)が、標準出力に出力されます。
09行:標準出力(ヘッダー + '.' + ペイロード)は、そのストリングに署名するためにopensslコマンドにパイプ接続されます。
10行:ストリングは、秘密鍵ファイル(/tmp内にあると想定)を使用して署名されます。また、ここで秘密鍵に明示的なパスを含めることもできます。
11行:バイナリー署名は、/tmp内の一意のストリーム ファイル名に書き出されます。
12行:バイナリー ストリーム ファイルが、Base64に変換されます。
13行:Base64出力は、変換コマンドにパイプ接続され、すべての改行および等号が削除され、次いでURLセーフでない文字がURLセーフな文字に変換されます。では、余計な単一引用符がこれだけあるのはなぜでしょうか。\n=のようなリテラルは、単一引用符で囲む必要があります。そして、RPGストリング内に単一引用符を追加するためには、単一引用符を2つにする必要があります。そしてさらに、単一引用符を含んでいるコマンドを実行するためには、その単一引用符も、2つにする必要があります。したがって、デバッグ時に確認すると、QShellCmd変数には、tr -d "\n=" というサブストリングが格納されています。
14行:Base64は、一意のファイル名にリダイレクトされます。これは、既存のファイルを効率的に切り詰め、それに新たな内容を書き込みます。一意のファイル名は再利用されます。
15行-16行:コマンドはSTRQSHでラッピングされて実行されます。堅牢な エラー処理が必要な場合は、代わりにCLプログラムでQShellコマンドを実行することもできます。
17行-18行:署名は、 IFS_READ 表関数を使用して、/tmpファイルからJwtSignature変数へ読み込まれます。
19行:ようやく、JWTを手にすることができました。JWTは、アクセス トークンと引き換えに、HTTPリクエストで認証サーバーに渡されます。
デバッグ
この時点で、新しいコードが本当に構文的に有効なJWTを構築しているかどうか確認しておくとよいでしょう。ここで、 jwt.io Webサイトの出番となります。まず、JWTのアルゴリズムを選択します(図4)。次いで、JSONのヘッダーおよびペイロードを、対応するセクションにペーストします(図8)。最後に、公開鍵および秘密鍵を、2つのスクロール可能テキスト領域にペーストします(もう一度、図8を参照)。
JWTが生成されて、表示されます。JWTの下に「Signature Verified(署名が検証されました)」と表示されたら完了です(図9)。また、コードによって生成されたJWTが、jwt.io Webページによって生成された値に一致することも検証します。値が一致しないJWTセクションがある場合は、それらをデバッグすることに集中します。JWTは正しいようだが、署名が無効という場合は、公開鍵と秘密鍵が正しいペアであり、それらをスクロール可能テキスト領域に正確にペーストしているか確認します。
アクセス トークン
それでは最後に、認証サーバーによって返されたHTTPレスポンスからアクセス トークンを抽出するコード スニペットについて見てみましょう。
01 dcl-s wkHttpBody varChar(5000);
02 dcl-s wkHttpResp varChar(5000);
03 dcl-s wkHttpStatus packed(3:0);
04 dcl-s wkAccessToken varChar(2000);
05 wkHttpBody = 'Build HTTP body here including the JWT';
06 // wkHttpResp = http_send_post(wkHttpBody);
07 wkHttpResp = '{"access_token":"eyJraWQi etc wAA8R1g","expires":300}';
08 If (wkHttpStatus = 200);
09 Exec SQL
SELECT COALESCE(temp_table.AccessToken, '')
INTO :wkAccessToken
FROM JSON_TABLE(:wkHttpResp, '$'
COLUMNS(
AccessToken VarChar(2000) PATH '$.access_token'
)
) as temp_table
Limit 1;
10 EndIf;
01行:認証サーバーに送信するHTTPボディ。フォーマットはサーバーによって決まりますが、ボディ内のどこかにJWTを含めることになります。
02行:認証サーバーからのHTTP JSONレスポンス。
03行:サーバーからの3桁のHTTPレスポンス ステータス。「200」であれば「OK」(成功)です。
04行:アクセス トークンを格納する変数。
05行:これはただの擬似コードです。認証サーバーの仕様に従ってHTTPボディを構築します。繰り返しになりますが、JWTはHTTPボディ内のどこかに入ることになります。
06行-07行:さらにこれも擬似コードです。自分で選んだHTTP関数を使用してください。
08行:「200」は、JWTが有効であり、JSONレスポンスでアクセス トークンが返されたことを意味します。
09行:JSON_TABLE 表関数を使用して、JSONからアクセス トークンを抽出します。
10行:終了のEndIf。
終わりに
この記事シリーズを書こうと思ったのは、IBM i 開発者用のツール集にJWTが抜けていると思ったことがきっかけでした。これらの記事を楽しんで読んでいただけたとしたら幸いです。4本の記事をすべて読み終えたら、JWTを作成し、それを認証サーバーに渡すための知識と自信が身に付いているはずです。そうしたHTTP POSTリクエストから、ボディにJSONフォーマットでURLエンコーディングまたはBase64を含めることを求められた場合に、それも処理することができます。もちろん、繰り返しのコードがあったらサービス プログラムに入れることをお勧めします。さて、これでおしまいです。楽しいコーディングを。