2. アクセストークン情報取得 / 内包型アクセストークンの検証

このビデオについて

このビデオは、2020 年 4 月 22 日に開催した弊社勉強会プレゼンテーション録画のパート 2 です。 アクセストークン情報の取得方法と、内包型アクセストークンの検証方法について、 Authlete の川崎貴彦がお話しします。

文字起こし(ログを元に再構成)

識別子型アクセストークンの情報取得方法

川崎: 次に、アクセストークンの情報の取得方法について話します。

識別子型アクセストークンの情報取得方法は、認可サーバに問い合わせるしかないんですね。 アクセストークン自体はそのまま意味不明な文字列になっているので、 認可サーバーが用意するイントロスペクション API に問い合わせることになります。

なので、まずリソースサーバーと認可サーバーがあるんですけど、 認可サーバー側でイントロスペクション API を用意して、 リソースサーバーが、自分が持っているアクセストークンを、イントロスペクション API に渡します。

そうするとイントロスペクション API の実装は、リクエストからアクセストークンを取り出して、 データベースなど検索するんですね。

そうしてアクセストークンが存在すれば、アクセストークンの情報がデータベースから得られるので、 リソースサーバー側に返す、という流れになります。

イントロスペクション API の標準仕様

このイントロスペクション API の標準が存在します。RFC 7662 という標準になっています。 仕様書を見ていくと、イントロスペクションリクエストのリクエストパラメーターにはこういうのがありますよ、と書いてあります。

2つパラメーターがありまして、ひとつは token という必須のバラメーターで、これにアクセストークンもしくはリフレッシュトークンを指定します。 もうひとつ、token_type_hint という、access_token もしくは refresh_token という値を取るんですけれども、  トークンリクエストパラメーターに指定したものが、アクセストークンなのかリフレッシュトークンなのか、 ヒントを与えるパラメーターです。

ただ、この token_type_hint リクエストパラメーターですけど、仕様書を読むと次のように書いてあります。

token_type_hint は、ただ認可サーバー側の検索の最適化のためだけに使うのであって、 認可サーバー側は使わなくてもいいし、無視してもいいし、 token_type_hint で渡されたヒントが access_token と書いてあるけれど、実際はリフレッシュトークンが渡ってきたと、 そのような齟齬があっても、問題なく処理を続けなくてはいけない、と仕様書に書かれています。

なので、token_type_hint と、実際に渡されたトークンの種類が異なっていても、エラーにならないばかりか、検索は続きます、というヒントです。

これがイントロスペクションリクエストの標準仕様です。

次に、イントロスペクションレスポンスのフォーマットですけど、JSON で返ってきます。 このような、表に書いたパラメーターが、その JSON の中に含まれています。

この中で必須なのが、active という真偽値のパラメーターです。 active が true の場合はそのアクセストークンは有効、false の場合は無効です。

有効とか無効とかの、厳密な実装上の定義は、 各認可サーバーの実装に依存します。 とはいえ、active の場合は、有効期限が切れてない、 無効にされていない、という意味になります。

active 以外には、どんなスコープにひもづいているか、 どのクライアントに対して発行されたか、 あとはトークンタイプがベアラーかどうかの情報、 あとは有効期限に関する情報とかもここに含まれています。 この表のような感じになっています。

イントロスペクションエンドポイントの保護

このイントロスペクションエンドポイントですけれども、 実はイントロスペクションリクエストに関して、仕様書に次のように書かれています。 エンドポイントをなんらかの方法で保護しないといけない、と。 誰でも彼でもイントロスペクションエンドポイントに アクセスできるようにしてはいけない、と書かれてるんですね。

ただ、ここに書かれているように、out of scope of this specification で、 どのようにそのエンドポイントを保護するかは 仕様の範囲外、と書かれています。 と、書かれてはいるんですけれども、 これは Authlete 社内で 3 年前にあった Slack のコピーなんですけれども、 僕が発言してるんですね。

仕様書を見ると、 アクセスの保護の方法が、任意とはいえ仕様書に書かれている例を見ると、 client authentication がどうのこうの書かれている、と。 続く例でも、クライアントの identifier と(書かれている)。 これだけ見ると、えっ、クライアント認証なの、って思うわけですね。

仕様書はイントロスペクションエンドポイントの保護方法の例として クライアント認証を挙げているので、 これを読んで僕は、 えっ、クライアントじゃなくてリソースサーバー認証じゃないの、って思ったわけです。

で、これについて議論を吹っかけたんですね。 議論を吹っかけた相手は Justin Richer という人で、 実はこの RFC 7662 を策定した本人です。この人に言ったわけです。

彼の返事はこんな感じで、 イントロスペクションエンドポイントを保護したくて、 それでなにか保護方法を考える必要があったんですけれど、 新しいのを考えるのが面倒だったので、 既存のクライアント認証を流用することにした、と。

仕様書には client_id とか client_secret とか出てくるけれど、 別にこれはクライアント認証ではなくて、 client_id のように見える何かと、client_secret のように見える何かでの認証をしている、と。

だけど API コーラーは OAuth クライアントではないですよ、と。 リソースサーバーですよ、という話をしてるんですね。

ただ、これはすごい混乱する話なんですよね。 混乱に拍車をかけている情報がもうひとつあります。

RFC 8414という仕様があって、 ここに認可サーバーのメタデータの定義がいろいろ書かれているんですね。

その中で、イントロスペクションエンドポイント系の、 メタデータを引っこ抜くと、こんな感じなんですけれども、 その 2 番目と 3 番目、 たとえば 2 番目っていうのは、introspection_endpoint_auth_methods_supported で イントロスペクションエンドポイントでサポートされる認証メソッドのリストを含むメタデータです。

これは、任意項目なんですけど、JSON 配列で、 client authentication methods supported by this introspection endpoint です、と。 このイントロスペクションエンドポイントによってサポートされる client authentication method のリストだ、と書いてます。 この定義だけを見ると、 クライアント認証以外の方法を書く余地が全くないように見えるわけです。

だから、本当は任意の方法でいいはずなのに、こんなこと書かれちゃうと、実装上も、 クライアント認証しかやれないじゃないですか。 こういう、誤解を与えるような記述がありまして、 イントロスペクションエンドポイントにおける認証方法に縛りはないはずなんですけど、 RFC 8414 のメタデータ定義だけを見ていると、 クライアント認証の流用がが前提となってしまっています、と。

このような状況で何が起こるかというと、 既存のクライアント管理テーブルを流用して、 そこにリソースサーバー用のレコードを追加する、 という認可サーバーの実装が出てくると。 どの実装がとは言いませんが、そういうのがあると。

こういう実装をすると何が起こるか。 クライアント A が、ある API に、 クライアント A 用のアクセストークンを持ってアクセスする、と。

リソースサーバーは、このアクセストークンをリクエストから取り出して、 このアクセストークンの情報が欲しいわけです。 情報が欲しいので、イントロスペクション API にアクセスするんですけれども、 自由にアクセスできないので、なにかしらの認証を受けなければいけないんです。

そのときに使うのが、このリソースサーバー用に発行された このリソースサーバーをクライアント B とみなして発行されたクライアント ID とクライアントシークレットを使って、 このイントロスペクション API にアクセスする、と。

認可サーバー側は、このクライアント B (リソースサーバー)の、 クライアント ID とクライアントシークレットは、 クライアント管理テーブルに入っているので、 OK で、処理を続行する、と。

イントロスペクションリクエストに含まれている、クライアント A 用のアクセストークンを取り出して、 データベースをルックアップして、クライアント A 用のアクセストークンの情報を取り出す、と。

これをリソースサーバーに返すんですね。 めでたくリソースサーバーはクライアント A 用のアクセストークンの情報をゲットできる、と。

ただ、これは何が起っているかというと、 クライアント B のクレデンシャルズを使って、クライアント A 用のアクセストークンの情報を取得できてしまってるんですね。 これって、任意のクライアントが他のクライアントの、 アクセストークンの情報を取得できてしまう、という実装になってしまうので、これは大きな問題だな、と。 こういうことが起こってしまっている、ということです。 これがちょっと問題です。

内包型アクセストークンの情報取得方法

それはさておき、次に、内包型アクセストークンの情報取得方法の話をします。 この情報取得方法は単純で、アクセストークンの中身を読む。

リソースサーバーが、 API コールに含まれているアクセストークンがありました、と。 その情報を知りたい場合は、このアクセストークンの中身を読めば、 情報が入っていると。

なので、イントロスペクション API を呼ばずに済むので、 認可サーバとの通信を発生せずに、かんたんに情報が取れる。 これが内包型アクセストークンの大きな利点で、 内包型が勝ったような感じになるんですね。 情報が、余計や通信を発生せずにある程度取得できる。

とはいえ、そうだと盲信していられるのも今のうちだけです。

たとえば内包型アクセストークンを失効させたい場合、 Public Key Infrastructure (PKI) の証明書の、CRL やら OCSP に相当するしくみが必要で、結局通信が発生します。 これについては後で述べます。

あと、内包型アクセストークンに必須となる署名です。 これもすぐ後で説明しますけれども、 場合によっては暗号化、 これを行うための鍵の有効性の確認のために、 けっきょく通信が発生するんですよね。

だから、内包型アクセストークンを使っていても、別に通信は発生してしまうので、 こういうのがあって、 内包型アクセストークンの情報取得時の有利な点、 要は、認可サーバーにアクセスしなくても、 情報がとれるというところを最大限に活用しようと思ったら、 アクセストークンの有効期間を短くして失効を諦める、 という妥協をしないと、この最大の利点を生かせません。 ここは後で詳細に説明をします。

内包型アクセストークンの検証

話に出てきた、内包型アクセストークンの検証の話をします。

内包型アクセストークンというのは、 暗号化されていない限り中身は誰でも読めてしまうので、 何らかの工夫をしないと、かんたんに偽造されてしまうんですね。

なのでリソースサーバーは、アクセストークンを受け取ったあと、 そのアクセストークンに含まれている情報を鵜呑みにする前に、 そのアクセストークンが偽造されたものではない、 ということを確認しなきゃいけない。

これについては RFC 6749 に示唆があって、 self-contained authorization information、と。 認可情報を自分自身に含んでいるんだけれども、 それは in a verifiable manner 、 検証可能な方法で情報を含んでいる、というのが示唆されているんです。 要は、検証可能ではない方法で含んでいても意味がない、ということです。

内包型アクセストークンで偽造を検出するには、 署名をつけて偽造検出できるようにするのが一般的な解決方法なんですね。

具体的にどうやるかっていうと、 認可サーバーがまず、アクセストークンの情報を表すデータを用意します。 スコープがこれだとか、クライアント ID がこれだ、と。データの塊を用意するんですね。

この用意したデータを入力として、署名用の鍵で署名を生成します。

このデータと署名を組み合わせます。 一緒にして、アクセストークンとして、クライアントに発行します。 このようにしたものをアクセストークンして発行する、と。

今度はクライアントアプリケーションが、このアクセストークンを添えて、 リソースサーバーの API を 呼ぶわけです。 こういうふうに。

リソースサーバーの API は、その API リクエストから アクセストークンを取り出して、 そのアクセストークンの署名を検証します。 検証用の鍵で署名を検証する。 署名が正しければ、 このアクセストークンは偽造されてない、ということがわかる。 こういう流れになる、ということです。

署名付きデータの汎用形式として、RFC 7519 で規定された JWT (ジョット) が、 非常に使い勝手が良いので、 内包型アクセストークンのフォーマットとして JWT が選ばれることが多いです。

実際、OAuth や OpenID Connect 関連の仕様書には、 アクセストークンの形式が JWT の場合にはこうしてね、と規定している仕様も、いくつか存在します。

たとえば RFC 8705 のセクション 3.1 を見ると、 アクセストークンが JSON Web Token (JWT) 形式で表現されている場合、 “x5t#S256” という、コンファメーションメソッドのメンバーを入れてくださいね、 という定義が書いてあります。 こんな感じで仕様書に書かれていることがあるんですね。

また、この JWT 形式のアクセストークンを標準化しよう、という動きもあります。 これはドラフトとして、JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens という仕様があります。