このビデオは、2020 年 4 月 22 日に開催した弊社勉強会のプレゼンテーション録画のパート 3 です。 JWT 型アクセストークンにおいて考慮すべき、署名アルゴリズムの選択肢、非対称鍵系署名アルゴリズム選定時の注意点、JWT 型アクセストークンの暗号化について、 Authlete の川崎貴彦がお話しします。
川崎: アクセストークンの実装方法として JWT を選ぶ場合の、 注意点、考えておかなくてはいけないポイントは、いくつあるので、 それをちょっと話していこうと思います。
JWT の署名アルゴリズムの選択肢としては、 RFC 7518 (JSON Web Algorithms) という仕様書の セクション 3.1に書かれているリストの値が、 ターゲットになると。 表にするとこんな感じなんですけども、 ただ、署名無しを意味する none は選択肢から外れます。 署名無しの JWT アクセストークンは、検証ができないと全く意味がないので、 選択肢から外れます。
その署名アルゴリズムですけど、 その署名アルゴリズムが対象鍵系の場合、 リソースサーバーと認可サーバーで、鍵を共有しなければいけません。
図の赤い鍵を共有しなくてはなりませんが、 対象鍵系の署名アルゴリズムのときの、鍵の決めかたの仕様が存在しないので、 対象鍵系の署名アルゴリズムを使いたい場合は、 実装ごとに自分たちでルールを決めなくてはなりません。
一方で、同じ共有鍵ですが、 クライアントと認可サーバーの間では、共有鍵の決め方にはルールがあります。
OpenID Connect Core 仕様の 10.1 Signing というセクションを見ると、 ID トークンやリクエストオブジェクトなどの署名に 対象鍵系アルゴリズムを用いる場合は、 クライアントシークレットを元にした値を、 鍵として使うこと、 と規定があります。
ただ、この規定はクライアントと認可サーバー間でしか使えないので、 認可サーバーとリソースサーバー間の 鍵の決めかたには流用できません。
次に、非対称鍵系署名アルゴリズム、 公開鍵系の署名アルゴリズムを選ぶ場合、 何が起こるかっていうと、 認可サーバは秘密鍵を持っていて、 それを用いてアクセストークンに署名をします。
リソースサーバーは公開鍵を用いて署名を検証します。 リソースサーバーは公開鍵を用いてその署名を検証しなくてはならないので、 何らかの方法で認可サーバの公開鍵を取得する必要があります。
この何らかの方法としては、標準的な方法がありまして、 認可サーバが自分自身の JWK セットドキュメントを、 公開するエンドポイントを作っておいて、 それをリソースサーバーが使えるようにしておけば、 その JWK セットドキュメントから、リソースサーバーは、 署名検証用の鍵を取得できます。
JWK セットを公開するエンドポイントはどこにあるかという情報は、 ディスカバリードキュメントの中に入っています。 ディスカバリードキュメントの中の jwks_uri という値の中に、 エンドポイントが書いてあります。
ディスカバリードキュメント自体がどこにあるかというと、 .well-known/openid-configuration というパスにあります。
字だけだと正直何を言っているかわからないと思うので、 同じことを図で説明します。
まず認可サーバーがあります。 これが、ディスカバリーエンドポイントというエンドポイントを実装します。 これは OpenID Connect Discovery 1.0 仕様に 定義されているエンドポイントです。
この仕様書に書かれているんですけど、 このエンドポイントはパス名が決まっています。 サーバー識別子の後ろに、 /.well-known/openid-configuration を、 書いたパスです。
サーバー識別子とは何かというと、 これは https で始まるものです。 サーバーが発行する JWT の iss クレームに入っていたり、 ディスカバリーエンドポイント内のイシュアーという値に入っているものです。
ここに、ディスカバリーエンドポイントがあります。 クライアントやリソースサーバーは、 まずディスカバリーエンドポイントに問い合わせをして、 設定情報をもらいます。
設定情報がいろいろ書かれています。 認可サーバーの設定が書かれてるんですけれども、 その中の一つの値として jwks_uri も含まれています。 これが JWK セットエンドポイントの URL を表しています。 その URL は認可サーバーの JWKS エンドポイントを示しているので、 ここにアクセスしに行きます。
アクセスすると JWK セットが返ってきて、 こんな感じで JSON が返ってくるんですね。 この JSON のフォーマットは RFC 7517 (JSON Web Key) という仕様に定義されています。
クライアントやリソースサーバーは、 こういう JWK セットドキュメントを取得しました。 ここにキーのリストが含まれているので、 この中から署名検証用の公開鍵を選んで取り出します。 こんな感じで公開鍵が取り出せます。
アクセストークンが JWT 形式で署名されています。 その署名を検証したい場合は、 クライアントやリソースサーバーは、 こんな手順で、 公開鍵を取る必要があります。
いま非対称鍵系署名アルゴリズムの話をしましたが、 そのアルゴリズムの選定方法です。
RFC 7518 のアルゴリズムのリストを見ると、 署名アルゴリズムのリストですね、 HS256 はサポート必須 (Required) です。 RS256が Recommended、 あと ES256 が Recommended+ になっていて、 あとは Optional です。
ここで HS256 は対称鍵系なので省くとして、 RS256 の話です。 この中でよく、実装として RS256 を選ぶことが多いです。 かつ、しかも、その実装によっては、 RS256 決めうちにしています。
なぜかというと、動的にいろいろなんでもできるようにするのが、 非常に実装が面倒なので、決めうちにしてやってるんですね。 たとえば Keycloak では、ちょっと前、1〜2 年前までは、 RS256 しか動かなかったです。
ただ、RS256 で始まるアルゴリズムは、 今となっては使用を避けたほうがいいとされています。 というのは、セキュリティ上の懸念があるためです。 Financial-grade API の Part 2 では、 RS 系のアルゴリズムはむしろ使用禁止になっています。 代わりに ES256 とか PS256 を推奨しています。
なので、たとえば、 アクセストークンの実装形式を JWT にします、 その署名アルゴリズムは何にしようかと考えているときに、 ES256 か PS256 を選ぶのがいいですよ、という話です。
変わって、今度は JWT アクセストークンの暗号化の話です。
対象鍵系暗号アルゴリズムを選んだ場合、 認可サーバーとリソースサーバーが同じ鍵を共有する必要があります。 共有鍵の決め方を定める標準仕様は存在しません。 署名のときの話と一緒です。 標準は存在しないので、自分たちでルールを決めて運用しなくてはいけません。
非対称鍵系暗号アルゴリズムですが、 注意しなければいけないのは、 暗号の場合、署名とは逆になるんですね。
何が起こるかというと、 公開鍵を持つのが認可サーバーになって、 秘密鍵を持つのがリソースサーバーになります。 だから公開鍵を持つのと、秘密鍵を持つのが、逆になるという話です。
認可サーバーは何らかの方法で、 リソースサーバーの公開鍵を取得する必要があります。 暗号化するために、リソースサーバーの公開鍵が必要です。 何らかの方法で取得しなくてはいけないんですが、 標準的な方法が残念ながら存在しません。
昔、リソースサーバーのメタデータを仕様で定めようという動きがありました。 その仕様の中に jwks_uri も含まれていました。 リソースサーバーの JWK セットのエンドポイントを定義しようという話がありました。 この仕様はその後の作業が進まず、 ドラフトが expire してしまって、もう無効になっています。 この仕様が存在していれば標準的な方法ができたんですが、 存在しないので、 つまり結果として、認可サーバーがリソースサーバーの公開鍵を取得する、 標準的な方法がありません。
これは復習ですが、 JWT を生成する側が暗号化したい場合に、 何が起こるかという話です。
署名は自分の秘密鍵を使えばできます。 暗号化する場合、 2段階暗号なので、まず共有鍵でまず1回目の暗号化をします。 この共有鍵自体を暗号化したいときです。
まず復号する側に、公開鍵と秘密鍵のペアを作ってもらいます。 そのうちの公開鍵を公開してもらわないといけません。 JWT を生成する側は、その公開鍵をゲットして、 この公開鍵を使って暗号化をします。 この暗号化されたものを、復号する側に渡します。 復号する側は、その暗号化されたものを、自分の秘密鍵を用いて復号します。 この共有鍵を用いて、さらに暗号を解きます。
このように、暗号化するときは、 署名ができたから暗号化もできるんじゃないかという、 かんたんな話じゃなくて、 鍵の関係が逆になるので、 途端に話が複雑になってきてしまいます。
なので、 ID トークンを暗号化する機能を提供している認可サーバーは、 実は少なくて、ID トークンの暗号化ができない認可サーバーの実装は、 たくさんあるんですね。 暗号化しようとすると、これだけ大変なことになるので、 みんな、そこまで手を出せません。
いまのが、JWT アクセストークンの暗号化に関する注意点です。
次に、アクセストークンのクレーム名の話です。
JWT アクセストークンには、 クレーム名とか、有効期限とか、クライアント ID などの、 アクセストークンに関する情報を埋め込まなくてはなりません。 どのようなクレーム名でペイロードに格納すべきかという話です。
ルールを自分で考えてもいいんですけれど、 RFC 7662 (Introspection) や RFC 7519 (JWT) の クレーム名や型に倣うのが無難でしょう。
あと、クレーム名の標準化を目指す動きもあります。 さっきも出ましたが、 JSON Web Token Profile for OAuth 2.0 Access Tokens という仕様書には、 クレーム名を共通化しよう、ということが書いてあります。
あと、仕様によっては、イントロスペクションレスポンス内のプロパティや、 JWT アクセストークン内のクレーム名を明示的に定めている仕様もあります。
たとえば RFC 8707 です。 この仕様は aud というクレーム名を定義しています。 あと RFC 8705 は、cnf というクレーム名の中に、 さらにその下に x5t#S256 みたいな感じのクレーム名を定義しています。
あと DPoP 仕様では、 cnf クレームの下に jkt クレームを定義しています。 あと Rich Authorization Requests 仕様では、 authorization_details という、ちょっと長い名前なんですけど、 クレーム名を定義しています。
いろんな仕様が、クレーム名をポンポンポン、と追加していくことがあります。 今ここに挙げた仕様は、後でさらっと触れます。