JWTトークンの取得と検証

🔲 JWTトークンの取得と検証

「openid-configuration」のエンドポイントから、JWTトークンの取得に関する「token_endpoint」へのアクセス方法がわかったことで、次にtoken_endpointにアクセスして、JWTトークンを取得しましょう。次にJWTトークンを取得後、トークンの中身やJWTトークンを検証してみたいと思います。

作成したnode-redのflowは以下の通りです。

token_endpointに関するスクリプトの内部は以下の通りです。

const host_ip = msg.host_ip;
const secret_key = msg.secret_key;
msg.payload = { "client_id": "test-client", 
                "client_secret": secret_key,
                "username": "banana",
                "password":"12345678",
                "grant_type" :"password" }
msg.headers = {"Content-Type":"application/x-www-form-urlencoded"};
const endpoint = "/realms/demo/protocol/openid-connect/token";
msg.url = host_ip + endpoint;
return msg;
  • パラメータは以下の通りです。
    • client_id   クライアントid名を指定します。
    • client_secret クライアントidで生成した「test-client」のシークレット鍵を指定します。
    • username  Realmの「demo」で生成したユーザ名「banana」を指定します。
    • password  bananaのパスワードを指定します。
    • grant_type  グラントタイプは、今回”password”を指定します。

上記client_secretは、下記の通りです。「Credential」からコピーしてください。

パラメータのheaderとして、「application/x-www-form-urlencoded」を指定します。
httpリクエストとしては、post methodで実行します。

上記エンドポイントへのアクセスが成功すること、下記の通りJWTトークンが発行されます。debugでみると「access_token」と「refresh_token」を取得した結果が確認できます。

取得したJWTトークンの中身を確認してみましょう。取得したAccess_tokenは、よくみると「.」で3つに区切れています。そこで、3つのデータに分割し、分割後のデータは、base64で復元することで、中身を確認することが可能です。

convert JWT tokenのスクリプト

const data = msg.payload.access_token;
var code = data.split('.');
msg.st1 = base64json.parse(code[0])
msg.st2 = base64json.parse(code[1])
msg.st3 = code[2]
msg.payload = code;
return msg;

JWTトークンの復元結果は以下の通りです。

ここで、「st1」に重要な項目が幾つかあります。

  • alg:”RS256″ 署名で採用された暗号アルゴリズムです。
  • typ:”JWT” トークンの種別です。JWTが指定されています。
  • kid:”lGjUlebSYsSuRXMuJIDWsE0XJV87-jlumjpy49Vc6Xo” JWTの署名検証で必要な公開鍵の識別IDです。複数公開鍵が存在しますので、この kidの値が一致する公開鍵を採用します。次の公開鍵の取得で使います。

JWT公開鍵の取得するスクリプトです。

const host_ip = msg.host_ip;
msg.url  = host_ip + "/realms/demo/protocol/openid-connect/certs";
return msg;

debugの取得結果です。公開鍵[keys: array[2]が2枚取得できています。公開鍵は、x5c: array[1]の配下にあります。下記のキャプチャは、「keys: 0」の内容です。

debugの取得結果です。公開鍵が2枚取得できています。下記のキャプチャは、「keys: 1」です。

ここで重要な鍵は、「kid」の値です。2枚の中から署名検証に必要な公開鍵を探します。「kid」の値が、「lGjUlebSYsSuRXMuJIDWsE0XJV87-jlumjpy49Vc6Xo」の鍵を使います。

ここで、公開鍵を含んだ証明書を取得するスクリプトは以下の通りです。msg.payload.keys[1].x5c[0]の記述により、2番目の証明書を取得しています。本来であれば、kidの検索プログラムを作成してください。ここでは、kidを説明するために配列の[0]と[1]の選択を表記しています。

const pubkey = msg.payload.keys[1].x5c[0]
const pub_pem = "-----BEGIN CERTIFICATE-----" +
pubkey +
"-----END CERTIFICATE-----"
msg.payload = pub_pem;
return msg;

取得した証明書をCertViewで見るとこんな感じです。Subjectは、CN=demoとなっています。

次に取得した証明書から公開鍵を抜き出します。公開鍵を抜き出すプログラムです。

const certPem = msg.payload;
const publickey = extractPublicKey(certPem);
msg.payload = publickey;

// 公開鍵を抽出する関数
function extractPublicKey(certPem) {
  try {
    // PEM形式の証明書から証明書オブジェクトを作成
    const cert = forge.pki.certificateFromPem(certPem);
    
    // 証明書から公開鍵を取得
    const publicKey = cert.publicKey;
    
    // 公開鍵をPEM形式に変換
    const publicKeyPem = forge.pki.publicKeyToPem(publicKey);
    
    return publicKeyPem;
  } catch (error) {
    throw new Error(`Failed to extract public key: ${error.message}`);
  }
}
return msg;

上記スクリプトでは、TLSの暗号化ツール「foge」を使っていますので、設定でインポートしてください。

上記functionを実行後、取得した公開鍵(pem形式)が以下の通りです。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqanEBKElbI7liSfBiKj
jMqIsCe81NR8R74jrCi9hVk9q9O5Fkhi09fwAGhhapMVX5ROxpP/l3S4gfsIeZkp
ODSfEUVUGciNQ2Jd+FT0iSg5IURTgmCkL9XqP9D8UMOKqgywT65lPgJI7m9jMeCP
3uzMlGCJAvGVgfAn3M/Pcj/TOIhMz/mnkwFVbyjbT1iqljEYosenReuSnqo7JL3b
tTcqcnLsZu1X6Fg/tSI7BjVd8JhDFWz8K5CFxkhNbuXIXI4hirxvFBZIUuL6Qq6i
a1hO1HpxiBPbcEuwqI5287zV1EQ77pfNQQnIdAuaymmnvT9MXINy+F2le9ebEbRg
DQIDAQAB
-----END PUBLIC KEY-----

今度は、取得した公開鍵を使って、JWTトークンの有効性を検証してみましょう。

検証プログラムで用意するのは、以下のデータです。

  • JWTの検証
    • access_token 上記JWTから引き出したアクセストークン
    • publicKey 上記のAPIコールで取得した公開鍵
    • algorithm [‘RS256’]を採用(署名で使用されたアルゴリズム)※st1:参照

JWTトークンの検証スクリプトは下記の通りです。veryfyJWT()で、access_tokenとpublicKeyを付与して検証しています。検証結果は、真偽、エラーも含めてjwt_decodeに返ってきます。このプログラムのポイントは、公開鍵です。取得に失敗していたら、Keycloakの管理コンソールから直接コピーしてください。

const token = msg.payload;
const access_token = token.access_token;
var jwt_decode = null; 
//公開鍵の取得
const publicKey = flow.get("pubkey");

msg.payload = verifyJWT(access_token, publicKey);
msg.jwt_decode = jwt_decode;
return msg;

// verifyJWT
function verifyJWT(access_token, publicKey) {
    try {
        // JWTの検証
        const decoded = jwt.verify(access_token, publicKey, { algorithms: ['RS256'] });
        jwt_decode = decoded;
        return true;
    } catch (error) {
        console.error('JWT verification failed:', error.message);
        jwt_decode = error.message;
        return false;
    }
}

Verify JWTには、下記の拡張モジュール「jsonwebtoken」をインポートしてください。

node-redのflowは下記の通りです。

JWT検証プログラムの実行結果です。JWTの検証結果は、「true」となっています。有効期限内での検証結果です。

ここで、実験として、証明書の有効期限内と失効した時間での検証比較をしてみましょう。

JWTのトークン発行から10分が経過した直後の検証結果です。失効した時間での検証となります。結果は、「false」となりました。jwtは失効したという検証結果です。Keycloakの設定では、JWTトークンの有効時間を10分に設定しています。

JWTの有効性検証では、下記のst2:で記述されている「exp: 2024/9/10 21:32:02 [UTC+9]」の時刻がポイントです。この時刻以降にJWTを検証すると結果は失効となります。

さらにJWTトークンを改ざんした場合の検証結果です。結果は、「false」となりました。つまり、偽物のJWTトークンを受け取っても、あるいは不正に改ざんされた場合であったとしても受け取ったJWTトークンを公開鍵を用いて検証することにより、JWTの安全性を確認することができます。

🔸 JWTトークンの取得とSSOの課題について

本章「JWTトークンの取得と検証」で、JWTトークンの取得と検証を行いましたが、ここで大きな問題がります。JWTトークンを取得するためにRealmの「demo」ユーザ名とパスワードを入力しています。つまり、JWTトークンを取得するために最も重要なユーザ名とパスワードをアプリケーション側が認証情報を要求しています。シングルテナント、シングルサーバ構成であれば、この状態でも問題はありませんが、マルチテナント、マルチサーバ構成のSSO(SSO:Single Sign On)では使えません。

ということは、、、、

Webアプリケーション側で、ユーザ名とパスワードという認証情報を知らなくても安全にJWTトークンを取得する仕組みが必要になります。

さらに1度のユーザー認証によって業務アプリケーションやクラウドサービスといった、複数のシステムの利用が可能になる仕組みが必要になります。Webサービス毎に認証が求められるサービスでは利用者にとっても不便です。つまり、認証と認可を分離した状態で、システムを構成する必要があります。

Authentication:認証 通信の相手が誰なのか?確認すること
Authorization:認可 リソースへのアクセス権限を与えること