Azure ADのIDトークンの署名を検証する

2021.09.13

いわさです。

前回はAzure ADにアプリ登録を行い、認証処理のリダイレクトによってOIDC IDトークンを取得することが出来ました。

カスタムクレームをIDトークンに含んでその情報を利用するなども可能ですが、実はこのままペイロード部分を使うわけにはいきません。

IDトークンはブラウザが送信したデータなので改ざんされている可能性があります。
前回の記事で解析したように、ヘッダー部分とペイロード部分はJSONデータをBase64URLエンコーディングしているだけなので、デコード→改ざん→再エンコードとすると改ざん出来てしまいます。
ではどうするかというと、JWTの署名部分を使って検証を行うことでIDトークンが改ざんされていないことを検証することが出来ます。

本日はこの、IDトークンの検証をおこなってみます。

署名と検証

前回の記事で触れたようにJWTはヘッダー.ペイロード.署名の形式で構成されています。
署名部分はヘッダーにある暗号化方式と鍵情報を用いて、ヘッダーとペイロード部分を結合したもので署名しています。

Azure ADで使うJWK(JSON Web Key)のセットは公開されており、以下を辿ることで確認出来ます。

https://login.microsoftonline.com/common/.well-known/openid-configuration

{
    "token_endpoint": "https://login.microsoftonline.com/common/oauth2/token",
    "token_endpoint_auth_methods_supported": [
        "client_secret_post",
        "private_key_jwt",
        "client_secret_basic"
    ],
    "jwks_uri": "https://login.microsoftonline.com/common/discovery/keys",
    "response_modes_supported": [
        "query",
        "fragment",
        "form_post"
    ],
    "subject_types_supported": [
        "pairwise"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "response_types_supported": [
        "code",
        "id_token",
        "code id_token",
        "token id_token",
        "token"
    ],
    "scopes_supported": [
        "openid"
    ],
    "issuer": "https://sts.windows.net/{tenantid}/",
    "microsoft_multi_refresh_token": true,
    "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/authorize",
    "device_authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/devicecode",
    "http_logout_supported": true,
    "frontchannel_logout_supported": true,
    "end_session_endpoint": "https://login.microsoftonline.com/common/oauth2/logout",
    "claims_supported": [
        "sub",
        "iss",
        "cloud_instance_name",
        "cloud_instance_host_name",
        "cloud_graph_host_name",
        "msgraph_host",
        "aud",
        "exp",
        "iat",
        "auth_time",
        "acr",
        "amr",
        "nonce",
        "email",
        "given_name",
        "family_name",
        "nickname"
    ],
    "check_session_iframe": "https://login.microsoftonline.com/common/oauth2/checksession",
    "userinfo_endpoint": "https://login.microsoftonline.com/common/openid/userinfo",
    "kerberos_endpoint": "https://login.microsoftonline.com/common/kerberos",
    "tenant_region_scope": null,
    "cloud_instance_name": "microsoftonline.com",
    "cloud_graph_host_name": "graph.windows.net",
    "msgraph_host": "graph.microsoft.com",
    "rbac_url": "https://pas.windows.net"
}

上記のjwks_uriのURLの先がキーセットとなっています。

https://login.microsoftonline.com/common/discovery/keys

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
            "x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
            "n": "oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ",
            "e": "AQAB",
            "x5c": [
                "MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R"
            ]
        },
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "l3sQ-50cCH4xBVZLHTGwnSR7680",
            "x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
            "n": "sfsXMXWuO-dniLaIELa3Pyqz9Y_rWff_AVrCAnFSdPHa8__Pmkbt_yq-6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS26nkKcNg5xfWxR2K8CPbSbY8gMrp_4pZn7tgrEmoLMkwfgYaVC-4MiFEo1P2gd9mCdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2imwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS_6ZIkUUlLUbG_rXWGl0YjZLS_Uf4q8Hbo7u-7MaFn8B69F6YaFdDlXm_A0SpedVFWQFGzMsp43_6vEzjfrFDJVAYkwb6xUQ",
            "e": "AQAB",
            "x5c": [
                "MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ"
            ]
        },
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "DqUu8gf-nAgcyjP3-SuplNAXAnc",
            "x5t": "DqUu8gf-nAgcyjP3-SuplNAXAnc",
            "n": "1n7-nWSLeuWQzBRlYSbS8RjvWvkQeD7QL9fOWaGXbW73VNGH0YipZisPClFv6GzwfWECTWQp19WFe_lASka5-KEWkQVzCbEMaaafOIs7hC61P5cGgw7dhuW4s7f6ZYGZEzQ4F5rHE-YNRbvD51qirPNzKHk3nji1wrh0YtbPPIf--NbI98bCwLLh9avedOmqESzWOGECEMXv8LSM-B9SKg_4QuBtyBwwIakTuqo84swTBM5w8PdhpWZZDtPgH87Wz-_WjWvk99AjXl7l8pWPQJiKNujt_ck3NDFpzaLEppodhUsID0ptRA008eCU6l8T-ux19wZmb_yBnHcV3pFWhQ",
            "e": "AQAB",
            "x5c": [
                "MIIC8TCCAdmgAwIBAgIQYVk/tJ1e4phISvVrAALNKTANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMjAxMjIxMDAwMDAwWhcNMjUxMjIxMDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWfv6dZIt65ZDMFGVhJtLxGO9a+RB4PtAv185ZoZdtbvdU0YfRiKlmKw8KUW/obPB9YQJNZCnX1YV7+UBKRrn4oRaRBXMJsQxppp84izuELrU/lwaDDt2G5bizt/plgZkTNDgXmscT5g1Fu8PnWqKs83MoeTeeOLXCuHRi1s88h/741sj3xsLAsuH1q9506aoRLNY4YQIQxe/wtIz4H1IqD/hC4G3IHDAhqRO6qjzizBMEznDw92GlZlkO0+AfztbP79aNa+T30CNeXuXylY9AmIo26O39yTc0MWnNosSmmh2FSwgPSm1EDTTx4JTqXxP67HX3BmZv/IGcdxXekVaFAgMBAAGjITAfMB0GA1UdDgQWBBQ2r//lgTPcKughDkzmCtRlw+P9SzANBgkqhkiG9w0BAQsFAAOCAQEAsFdRyczNWh/qpYvcIZbDvWYzlrmFZc6blcUzns9zf7sUWtQZrZPu5DbetV2Gr2r3qtMDKXCUaR+pqoy3I2zxTX3x8bTNhZD9YAgAFlTLNSydTaK5RHyB/5kr6B7ZJeNIk3PRVhRGt6ybCJSjV/VYVkLR5fdLP+5GhvBESobAR/d0ntriTzp7/tLMb/oXx7w5Hu1m3I8rpMocoXfH2SH1GLmMXj6Mx1dtwCDYM6bsb3fhWRz9O9OMR6QNiTnq8q9wn1QzBAnRcswYzT1LKKBPNFSasCvLYOCPOZCL+W8N8jqa9ZRYNYKWXzmiSptgBEM24t3m5FUWzWqoLu9pIcnkPQ=="
            ]
        },
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "OzZ5Dbmcso9Qzt2ModGmihg30Bo",
            "x5t": "OzZ5Dbmcso9Qzt2ModGmihg30Bo",
            "n": "01re9a2BUTtNtdFzLNI-QEHW8XhDiDMDbGMkxHRIYXH41zBccsXwH9vMi0HuxXHpXOzwtUYKwl93ZR37tp6lpvwlU1HePNmZpJ9D-XAvU73x03YKoZEdaFB39VsVyLih3fuPv6DPE2qT-TNE3X5YdIWOGFrcMkcXLsjO-BCq4qcSdBH2lBgEQUuD6nqreLZsg-gPzSDhjVScIUZGiD8M2sKxADiIHo5KlaZIyu32t8JkavP9jM7ItSAjzig1W2yvVQzUQZA-xZqJo2jxB3g_fygdPUHK6UN-_cqkrfxn2-VWH1wMhlm90SpxTMD4HoYOViz1ggH8GCX2aBiX5OzQ6Q",
            "e": "AQAB",
            "x5c": [
                "MIIC8TCCAdmgAwIBAgIQQrXPXIlUE4JMTMkVj+02YTANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMjEwMzEwMDAwMDAwWhcNMjYwMzEwMDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTWt71rYFRO0210XMs0j5AQdbxeEOIMwNsYyTEdEhhcfjXMFxyxfAf28yLQe7Fcelc7PC1RgrCX3dlHfu2nqWm/CVTUd482Zmkn0P5cC9TvfHTdgqhkR1oUHf1WxXIuKHd+4+/oM8TapP5M0Tdflh0hY4YWtwyRxcuyM74EKripxJ0EfaUGARBS4Pqeqt4tmyD6A/NIOGNVJwhRkaIPwzawrEAOIgejkqVpkjK7fa3wmRq8/2Mzsi1ICPOKDVbbK9VDNRBkD7FmomjaPEHeD9/KB09QcrpQ379yqSt/Gfb5VYfXAyGWb3RKnFMwPgehg5WLPWCAfwYJfZoGJfk7NDpAgMBAAGjITAfMB0GA1UdDgQWBBTECjBRANDPLGrn1p7qtwswtBU7JzANBgkqhkiG9w0BAQsFAAOCAQEAq1Ib4ERvXG5kiVmhfLOpun2ElVOLY+XkvVlyVjq35rZmSIGxgfFc08QOQFVmrWQYrlss0LbboH0cIKiD6+rVoZTMWxGEicOcGNFzrkcG0ulu0cghKYID3GKDTftYKEPkvu2vQmueqa4t2tT3PlYF7Fi2dboR5Y96Ugs8zqNwdBMRm677N/tJBk53CsOf9NnBaxZ1EGArmEHHIb80vODStv35ueLrfMRtCF/HcgkGxy2U8kaCzYmmzHh4zYDkeCwM3Cq2bGkG+Efe9hFYfDHw13DzTR+h9pPqFFiAxnZ3ofT96NrZHdYjwbfmM8cw3ldg0xQzGcwZjtyYmwJ6sDdRvQ=="
            ]
        }
    ]
}

私が発行したサンプルトークンのヘッダーは以下です。

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}

キーセットのうち、kidがl3sQ-50cCH4xBVZLHTGwnSR7680のものが、今回必要なJWKということですね。

つまり、検証者はこのキーセットにアクセスする必要があります。
そして、Azure ADのキーセットについては以下のように案内されています。

Azure AD は定期的に使用可能なキー セットをローテーションするので、このキー変更を自動的に処理するようにアプリを作成する必要があります。 Azure AD によって使用される公開キーの更新を確認する適切な頻度は、24 時間間隔です。

ツールで簡易的に

JWTの解析は以下などを使うことで行うことも可能です。

JWT.MS(Microsoftより提供)

JWT.IO(Auth0より提供)

特に、jwt.ioはすごいです。
署名の検証までやってくれます。

改ざんしたトークンを突っ込んでも、署名エラーが検出出来ていますね。

どういう仕組でキーを取りにいっているのだろう。主要ベンダーのリストを集めている?JWTからキーセットまで辿れるんでしたっけ…。
ここは個別に取得方法を考えなければいけないところだと思ってました。キーセット取得までJWTのみで辿れるのであればどなたかご存知の方いらっしゃれば教えて頂けると嬉しいです。

実際にはWebサービスにJWTをアップロードして検証することが難しかったり、自前のプログラム上で検証処理を組み込まなければいけないことが多いと思います。
次では検証処理を実装してみます。

処理上で検証する

自前で、といっても検証アルゴリズムを組み込んだりはせず、ライブラリを使います。

各言語やランタイムごとに様々なライブラリが存在しています。
今回はAWSのCoginio IDトークンの検証ドキュメントでも推奨されている、JWKとPEMに変換し、検証ライブラリで検証を行う方法を使ってみました。

使い方はUsageに記載のままの実装で良いです。
便宜上JWKとJWTは手動で貼り付けました。

var jwkToPem = require('jwk-to-pem'),
jwt = require('jsonwebtoken');
var jwk = {
    "kty": "RSA",
    "use": "sig",
    "kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
    "x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg",
    "n": "oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ",
    "e": "AQAB",
    "x5c": [
        "MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R"
    ]
};
var pem = jwkToPem(jwk);
console.log(pem);

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imwzc1EtNTBjQ0g0eEJWWkxIVEd3blNSNzY4MCJ9.eyJhdWQiOiJhNjlhOGI3Yy0yMzZmLTRiMTEtODI1Ni1jNzM0YTg4NGMyZGYiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOWI5ZTJmYzYtZGMxYS00ZDdmLTk3ZmYtZTg2NjAwYWM1YjQ4L3YyLjAiLCJpYXQiOjE2MzE0NTM1MjUsIm5iZiI6MTYzMTQ1MzUyNSwiZXhwIjoxNjMxNDU3NDI1LCJhaW8iOiJBWVFBZS84VEFBQUF6WUJ0a1AwOFhNdmdDa09rNVc0RDlsb3ZIbEdGUlBGSEN2TGREekMybUkrdE5idFlOZGpad1NjRkQwbG5ncVR5SWpUQWZrRDc0K1g4VWtFVW1jb2tMQ1BLMVJzWE11Rjl6RTZJMXBnZTcrdGpmTUFhL0dnZm5yKzJKTjJDOGY2U0FXOVNEMHVGeDRWT1lra3Z3cDEvdERVYTNPUnFSNk9MNHRqSjY0WmhOYTA9IiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkLyIsIm5hbWUiOiLlsqnmtYUg6LK05aSnIiwibm9uY2UiOiI2Nzg5MTAiLCJvaWQiOiI2MmU3OGM0Ny02Y2ZmLTRhNWMtOWQ3ZS00NWQzMTkxYTAzOTMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJpd2FzYS5henVyZUBnbWFpbC5jb20iLCJyaCI6IjAuQVhBQXhpLWVteHJjZjAyWF8taG1BS3hiU0h5TG1xWnZJeEZMZ2xiSE5LaUV3dDl3QUxvLiIsInN1YiI6IlZnaWU5eDlGUVo1Y2R1QU5kcEhIa2lzSmVKR0wxcFdQMGt6TmxTVElfOWMiLCJ0aWQiOiI5YjllMmZjNi1kYzFhLTRkN2YtOTdmZi1lODY2MDBhYzViNDgiLCJ1dGkiOiI4RXFiMnR3X3cwU2VuckozUWVaNUFBIiwidmVyIjoiMi4wIn0.TQ3_29Zo95nCOpGxZJjazi7Xw5AMzrpDRDtBIMhsnhAK7VCeGHl1E5cv2XmJsOyGxYVLX4sTQ1eNi07i88MdB2fA2-_zXA39NaGAhgWXXxSiUUf3Ve2rXKV51qd5xN2S-ygHDk8HruP6wbdb-ioH7HsyvtueSQ2Tv1_IV5gWSSRdEusS2PGPdmjJpGjzW8ZIwes1W_0kxiCsLN_qOzm_ixas3Eu44yrDV5aVu9G_8vce29QxFhH1HNLsXlbVKKBPulg5_2Hg5P1AjVDL1uhxS5obD-4Ap2Bf4ESoY5eUDxghvPaxK8o1_omhksU8CQcOWtzU4Kwuu1SolHjspFmYnQ";
console.log(jwt.verify(token, pem));
iwasa.takahito@hoge verify % node hogehoge.js
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsfsXMXWuO+dniLaIELa3
Pyqz9Y/rWff/AVrCAnFSdPHa8//Pmkbt/yq+6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS
26nkKcNg5xfWxR2K8CPbSbY8gMrp/4pZn7tgrEmoLMkwfgYaVC+4MiFEo1P2gd9m
CdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2i
mwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS/6ZIkUUlLUbG/rXWGl0YjZLS/Uf4q
8Hbo7u+7MaFn8B69F6YaFdDlXm/A0SpedVFWQFGzMsp43/6vEzjfrFDJVAYkwb6x
UQIDAQAB
-----END PUBLIC KEY-----

{
  aud: 'a69a8b7c-236f-4b11-8256-c734a884c2df',
  iss: 'https://login.microsoftonline.com/9b9e2fc6-dc1a-4d7f-97ff-e86600ac5b48/v2.0',
  iat: 1631460042,
  nbf: 1631460042,
  exp: 1631463942,
  aio: 'AYQAe/8TAAAALcDdrsfshc+1pi2yAWfQZmV+Tazf+sZl0i7EGYIaCkiWU8dsIfRMpsp8gwD9QKOMoMZlygN9/8Ysf3GJdYzeLaHqpJskjqF90L37BbexE5pQgV4EGMJzFKKwHLJGbDqgv0MgWkkTsPdHWCQ5Blpp8JRF6GorYi+6lc2r8s+9H8c=',
  idp: 'https://sts.windows.net/9188040d-6c67-4c5b-b112-36a304b66dad/',
  name: '岩浅 貴大',
  nonce: '678910',
  oid: '62e78c47-6cff-4a5c-9d7e-45d3191a0393',
  preferred_username: 'iwasa.hoge@mail.com',
  rh: '0.AXAAxi-emxrcf02X_-hmAKxbSHyLmqZvIxFLglbHNKiEwt9wALo.',
  sub: 'Vgie9x9FQZ5cduANdpHHkisJeJGL1pWP0kzNlSTI_9c',
  tid: '9b9e2fc6-dc1a-4d7f-97ff-e86600ac5b48',
  uti: 'DPFRurPR4kG-wu1BZdCEAA',
  ver: '2.0'
}

検証出来ました。

不正なトークンでも確認してみました。
JsonWebTokenError: invalid signatureが発生しますね。

JsonWebTokenError: invalid signature
    at /Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:133:19
    at getSecret (/Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:90:14)
    at Object.module.exports [as verify] (/Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:94:10)
    at Object.<anonymous> (/Users/iwasa.takahito/work/verify/hogehoge.js:21:5)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_modul

ちなみに、トークンの期限が切れている場合は、TokenExpiredError: jwt expiredが発生しました。
ライブラリすごいな。

TokenExpiredError: jwt expired
    at /Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:152:21
    at getSecret (/Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:90:14)
    at Object.module.exports [as verify] (/Users/iwasa.takahito/node_modules/jsonwebtoken/verify.js:94:10)
    at Object.<anonymous> (/Users/iwasa.takahito/work/verify/hogehoge.js:19:5)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
    at internal/main/run_main_module.js:17:47 {
  expiredAt: 2021-09-12T14:37:05.000Z
}

さいごに

ここでは、JWTの署名検証を行いました。
なお、署名検証をして改ざんされていないことを確認したのちは、トークンが有効であるか(対象アプリか、期限はどうか、など)を検証する必要があります。

以下の JWT クレームは、トークン上の署名を検証した後、ID トークンで検証する必要があります。 これらのクレームは、トークン検証ライブラリでも検証できます。

タイムスタンプ: iat、nbf、exp の各タイムスタンプがすべて、現在の時刻の前か後であることが必要です (該当する場合)。 対象: aud 要求がアプリケーションのアプリ ID と一致する必要があります。 nonce: ペイロードの nonce 要求が、最初の要求で /authorize エンドポイントに渡された nonce パラメーターと一致する必要があります。