OAuth
Authlete

OAuth アクセストークンの実装に関する考察


1. アクセストークン実装方法の分類

OAuth のアクセストークン※1の実装方法は認可サーバーの実装依存です。

実装依存ではありますが、RFC 6749 の『1.4. Access Token』にある次の記述が示唆するように、


The token may denote an identifier used to retrieve the authorization information or may self-contain the authorization information in a verifiable manner (i.e., a token string consisting of some data and a signature). ...


アクセストークンの実装方法はおおむね『識別子型』と『内包型』の二つに大別することができます。そして、これらを組み合わせる『ハイブリッド型』の実装もあります。

※1:アクセストークンの概念については『一番分かりやすい OAuth の説明』を参照してください。


1.1. 識別子型

識別子型の実装方法では、アクセストークンに紐付く情報を認可サーバーのデータベース内に保存します。そして、そのデータレコードを一意に特定できる識別子をアクセストークンとします※2

access_token-identifier_type.png

※2:実際の実装では、データレコードにはアクセストークンそのものではなくアクセストークンのハッシュ値を保存することになると思います。


1.2. 内包型

一方、内包型の実装方法では、アクセストークンに紐付く情報をアクセストークン自体の中に埋め込みます

access_token-self_container_type.png


1.3. ハイブリッド型

ハイブリッド型の実装方法では、内包型アクセストークンを生成しつつも、それに付随するデータを認可サーバーのデータベース内に持ちます

access_token_hybrid_type.png


2. アクセストークン情報取得方法


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

アクセストークンが識別子型の場合、アクセストークンの情報を取得するために、リソースサーバーは認可サーバーに問い合わせをしなければなりません※3。問い合わせ用に認可サーバーが用意する API はイントロスペクション API(Introspection API)と呼ばれます。

access_token_info-identifier_type.png

イントロスペクション API には、RFC 7662 という標準仕様が存在します。

※3:認可サーバーとリソースサーバーがデータベースを共有しているシステムでは、リソースサーバーはアクセストークンの情報を取得するために直接データベースを参照します。


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

アクセストークンが内包型の場合、アクセストークンの中身を読めば、アクセストークンに紐付く情報(例えば「有効期限」)を取得することができます。

access_token_info-self_container_type.png


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

内包型アクセストークンは、暗号化されていない限り、そのフォーマットは公知となってしまいます。そのため、何かしら工夫をしなければ、簡単に偽造されてしまいます。

リソースサーバーは、受け取ったアクセストークンに埋め込まれている情報を鵜呑みにする前に、そのアクセストークンが偽造されたものではないことを何かしらの方法で検証しなければなりません。前掲した RFC 6749 の『1.4. Access Token』からの引用部に “in a verifiable manner” と書いてあるのはそれが理由です。

偽造されていないことを検出する一般的な方法は、生成したデータに署名をつけ、データ利用時にその署名を検証する方法です。内包型アクセストークンの場合でいうと、認可サーバーが署名付きアクセストークンを生成し、リソースサーバーがその署名を検証する、という手順となります。

access_token_with_signature.png

署名付きデータの汎用形式として、RFC 7519 で規定される JWT(JSON Web Token)の使い勝手が良いため、内包型アクセストークンのフォーマットとして JWT が選ばれることが多いです。実際、それを前提としている記述を含む仕様書も存在します。


4. JWT 型アクセストークン実装時の考慮事項

アクセストークンの実装方法として JWT を採用する場合に考慮すべき点を挙げます。


4.1. 署名アルゴリズム

署名アルゴリズムの選択肢は RFC 7518 の『3.1. "alg" (Algorithm) Header Parameter Values for JWS』に列挙されているものになります。ただし、『署名無し』を意味する none は選択肢から外れます。

alg
アルゴリズム

HS256
HMAC using SHA-256

HS384
HMAC using SHA-384

HS512
HMAC using SHA-512

RS256
RSASSA-PKCS1-v1_5 using SHA-256

RS384
RSASSA-PKCS1-v1_5 using SHA-384

RS512
RSASSA-PKCS1-v1_5 using SHA-512

ES256
ECDSA using P-256 and SHA-256

ES384
ECDSA using P-384 and SHA-384

ES512
ECDSA using P-521 and SHA-512

PS256
RSASSA-PSS using SHA-256 and MGF1 with SHA-256

PS384
RSASSA-PSS using SHA-384 and MGF1 with SHA-384

PS512
RSASSA-PSS using SHA-512 and MGF1 with SHA-512

none
No digital signature or MAC performed


4.1.1. 対称鍵系署名アルゴリズム

HS256HS384HS512 は対称鍵系アルゴリズムなので、認可サーバー(JWT 型アクセストークンを生成する側)とリソースサーバー(JWT 型アクセストークンを解釈する側)が同じ鍵を共有する必要があります。この共有鍵の決め方を定めている標準仕様は現在のところ存在しません。

OpenID Connect Core 1.0』の『10.1. Signing』には、「ID トークンリクエストオブジェクトなどの署名に対称鍵系アルゴリズムを用いる場合、クライアントシークレットを鍵として用いること」という旨の規程があります。しかしこの規程は、認可サーバーとクライアントの間でしか成立しないので、認可サーバーとリソースサーバーの間で共有する鍵の決め方に流用することはできません。

keys_for_signing_with_symmetric_algorithm.png

上記のことから、署名アルゴリズムに対称鍵系を用いる場合、共有鍵の決め方については実装者が独自に定める必要があります。


4.1.2. 非対称鍵系署名アルゴリズム

他のアルゴリズムは非対称鍵系アルゴリズムになります。

認可サーバーは秘密鍵を用いてアクセストークンに署名し、リソースサーバーは認可サーバーの公開鍵を用いて署名を検証します。必然的に、署名検証に先立ち、リソースサーバーは何らかの方法で認可サーバーの公開鍵を取得しておく必要があります。

認可サーバーが自身の JWK Set(RFC 7517)を公開するエンドポイントを提供し、その JWK Set の中にアクセストークンの署名を検証するための公開鍵を含めているのであれば、リソースサーバーはそのエンドポイントから署名検証用の公開鍵をダウンロードすることができます。

JWK Set を公開するエンドポイントの URL については、もしも認可サーバーが『OpenID Connect Discovery 1.0』をサポートしているのであれば、その認可サーバーのディスカバリーエンドポイント(認可サーバーの識別子/.well-known/openid-configuration)から情報を取得することができます。ディスカバリーエンドポイントは、サーバーの設定情報を JSON 形式で返します。その JSON の中にある jwks_uri というパラメーターの値が JWK Set を公開するエンドポイントの URL を示しています。(例:Google のディスカバリーエンドポイント

steps_to_get_public_key.png

RFC 7518 では RS256 を Recommended(推奨)としていますが、非対称鍵系アルゴリズムのうち RS で始まるものの使用は避けたほうがよいでしょう。セキュリティー上の懸念があることから、『Financial-grade API - Part 2: Read and Write API Security Profile』の『8.6. JWS algorithm considerations』では、RS 系アルゴリズムを使うべきではないとしています。また、鍵のサイズやパフォーマンスの観点からも、他のアルゴリズムのほうが好ましいです。


4.2. 暗号化

RFC 7516 を用いて、JWT 型アクセストークンを暗号化することは可能です。


4.2.1. 対称鍵系暗号アルゴリズム

認可サーバーとリソースサーバーの間で共有する鍵の決め方については、対称鍵系署名アルゴリズムと同様、仕様は存在しないので、実装者が独自に定める必要があります。


4.2.2. 非対称鍵系暗号アルゴリズム

暗号化に非対称鍵系アルゴリズムを用いる場合、認可サーバーは、リソースサーバーの公開鍵を使って暗号処理をすることになります。ただし、リソースサーバーの公開鍵の取得方法については、標準仕様は存在しません※4。そのため、リソースサーバーの公開鍵を認可サーバーに渡す方法については、実装者が独自に定める必要があります。

アクセストークンの暗号化に非対称鍵系アルゴリズムを用いる場合、認可サーバーのイントロスペクションエンドポイントの実装では、そのアクセストークンを復号するためにリソースサーバーの秘密鍵が必要となります。これを問題と考えるならば、イントロスペクションエンドポイントの実装の選択肢は、(1)暗号化されたアクセストークンが渡されてきた場合はエラーを返す、(2)そもそもイントロスペクションエンドポイントを提供しない、のどちらかになります。

※4:リソースサーバーのメタデータを定める仕様(OAuth 2.0 Protected Resource Metadata)が提案され、メタデータの一つとして jwks_uri が含まれていましたが、当ドラフトの最終更新日は 2 年以上前の 2017 年 1 月 19 日であり、ドラフト自体は既に無効となっています。


4.3. クライアントから隠蔽すべき情報

暗号化していない JWT 型アクセストークンの場合、ペイロード部分は簡単に読めてしまいます。そのため、クライアントに知られたくない情報をアクセストークン内に含めてはいけません。

secret_info_in_unencrypted_access_token.png

クライアントに知られたくない情報をアクセストークンに紐付けたい場合、次のいずれかの方法をとることになると思います。


  1. アクセストークンを暗号化する。


  2. 識別子型アクセストークンを利用する。


  3. ハイブリッド型アクセストークンを利用し、秘密にしたい情報をアクセストークンには含めず、サーバー側のデータベース内のみに保存する。



4.4. アクセストークン失効

不可能ではないですが、内包型アクセストークンの失効は難しいです。構造的には PKI 証明書と同じなので、期限切れ前に内包型アクセストークンを失効させるためには、PKI の CRL(Certificate Revocation List)や OCSP(Online Certificate Status Protocol)相当の仕組みを運用するか、もしくは署名検証用の鍵自体を失効しなければなりません。ただし後者の方法は他のアクセストークンも一緒に失効されてしまいます。

CRL や OCSP 相当の仕組みを構築するためには、まず、アクセストークンを一意に識別できるようにする必要があります。これは、JWT の jti クレームを使えば実現できます。そして、アクセストークンを失効させるたびに、その一意識別子を「失効されたアクセストークンのリスト」に追加します。その一意識別子は、当該アクセストークンの元々の有効期限が切れるまでは失効リスト内に保持しておかなければなりません。

アクセストークンを受け取ったリソースサーバーは、そのアクセストークンの失効状態を確認しなければなりません。CRL 相当の仕組みであれば、どこかから失効されたアクセストークンのリストをダウンロードし、その中に受け取ったアクセストークンの一意識別子が含まれているかどうかを確認することになります。一方、OCSP 相当の仕組みであれば、OCSP レスポンダに相当する「アクセストークンの失効状態を返す API」に、受け取ったアクセストークンの一意識別子を渡して失効状態を教えてもらうことになります。

access_token_revocation_status_api.png

しかしながら、アクセストークンの失効状態を確認するために認可サーバーに問い合わせをすると、識別子型アクセストークンにおいてイントロスペクション API に問い合わせをするのと同様に、ネットワーク通信が発生してしまいます。これでは、内包型アクセストークンの最大の利点が損なわれてしまいます。識別子型アクセストークンのほうが利点が多いことを考慮すると、内包型アクセストークンを積極的に採用する理由がほとんどなくなってしまいます※5

そのため、内包型アクセストークンを採用する場合は、「アクセストークンの有効期間を短くし、失効を諦める」という妥協をすることが多くなると思われます。

※5:これを差し置いても内包型アクセストークンを採用しなければならない理由があるとすれば、それは「何らかの事情によりリソースサーバーと認可サーバーが通信できない」場合だと思われます。


4.5. クレーム


4.5.1. クレーム名

アクセストークンの情報、例えば、スコープ、有効期限、クライアント ID、などをどのようにペイロード部に格納するかについては、現在のところ標準仕様はありません※6

例えば、アクセストークンに紐付くスコープ群の表し方としては、(1)クレーム名を scopes とし、その値を配列にする、

"scopes" : [ "email", "profile" ]

という方法や、(2)クレーム名を scope とし、スコープ名をスペースで連結した単一文字列にする、

"scope" : "email profile"

といった方法など、幾つか考案することができます。無難な選択をするのであれば、RFC 7662RFC 7519 で定められているクレーム名と型に従うのがよいでしょう。

※6:第四回 OAuth Security Workshop提案がありましたが、一見して考慮不足が明らかで参加者からだいぶ叩かれたようです(たたき台だけに)。仕様書形式のドラフトはこちら


4.5.2. 証明書バインディング

OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens』(以降 MTLS)という仕様書には、トークンリクエストの際にクライアントが提示したクライアント証明書を、発行するアクセストークンと紐付ける仕様が含まれています(参考:『クライアント証明書に紐付くトークン』)。

アクセストークンがクライアント証明書と紐付いており、かつ、アクセストークンが JWT で表現されている場合、その JWT はそのクライアント証明書のハッシュ値をペイロード部に含めるべき、と当仕様書は述べています。具体的には、クライアント証明書の X.509 Certificate SHA-256 Thumbprint を x5t#S256 という名前で、cnf クレーム(参考:RFC 7800)内に含めるべきと述べています。

下記は当仕様書の『3.1. JWT Certificate Thumbprint Confirmation Method』から抜粋した例です。

{

"iss": "https://server.example.com",
"sub": "ty.webb@example.com",
"exp": 1493726400,
"nbf": 1493722800,
"cnf":{
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
}
}

cnf クレームは、JWT 形式のアクセストークンのペイロード部で用いるクレームの名前について具体的に定めている例となります。


4.5.3. ユーザー情報に含めるクレーム

OpenID Connect Core 1.0』の『5.3. UserInfo Endpoint』では、ユーザー情報エンドポイントというエンドポイントが定義されています。このエンドポイントに openid スコープを含むアクセストークンを渡すと、当該アクセストークンに紐付くユーザーのユーザー情報が返ってきます。

ユーザー情報エンドポイントから返されるユーザー情報に含めて欲しいクレーム群は、認可リクエスト時に要求することができます。要求方法には『5.4. Requesting Claims using Scope Values』で定義される方法と『5.5. Requesting Claims using the "claims" Request Parameter』で定義される方法の二つがあります。

ここで重要なのは、「ユーザー情報エンドポイントからの応答に含めるべきクレーム群をアクセストークンに紐付けて覚えておかなければならない」という点です。論理的には、認可サーバー側に対応するデータレコードを持たない内包型アクセストークンでは、その内部に「ユーザー情報エンドポイントからの応答に含めるべきクレーム群」の情報を含んでいなければならないということです。

例えば、認可リクエストの scope パラメーターの値が openid phone であり、claims パラメーターの値が次の JSON であった場合、

{

"userinfo": {
"given_name": {"essential": true},
"nickname": {"value": "Bob"}
}
}

結果として生成されるアクセストークンは、下記の例における userinfo と同等の情報を含んでいる必要があります※7

{

"iss": "https://server.example.com",
"sub": "ty.webb@example.com",
"exp": 1493726400,
"nbf": 1493722800,
"cnf":{
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
},
"userinfo":{
"phone_number": null,
"phone_number_verified": null,
"given_name": {"essential": true},
"nickname": {"value": "Bob"}
}
}

※7:scopephone を含めることは、phone_number クレームと phone_number_verified クレームを要求することと同じです。詳細は『5.4. Requesting Claims using Scope Values』を参照してください。


コラム:潜在的な個人情報流出

論理的な結論は、「内包型アクセストークンは、ユーザー情報エンドポイントから返してほしいと要求されたクレーム群に関する情報を内包しなければならない」ということになりますが、この結論について、Authlete 社内の議論が興味深かったので紹介します。下記の会話で justin は『OAuth 2 in Action』(邦訳:OAuth 徹底入門)の著者の Justin Richer で、taka はです。


justin> Your conclusion on JWt-based access token is incomplete. You do not need to put the user info into the access token, nor should you do so as it is potentially privacy-leaking. The userinfo endpoint will need to dereference the JWT to determine which user it applies to. This can be done with the sub claim. If the claims parameter is to be supported directly as you describe above, then that can be looked up underneath the jti claim which is transaction-specific as Joseph said. No JWT based system is fully self-contained, that I’ve seen. Any those that come closest use encrypted tokens (and the associated key management) to keep information relatively safe.

taka> Privacy-leaking only if claims in claims contain value or values.

justin> Not really — the fact that a field exists at all could be privacy leaking. This is less of a problem with common things like “address” and “family name” but more of an issue once you get to other resource types like medical and financial records.

taka> "No JWT based system is fully self-contained" is an interesting statement.

justin> You are right in the common case, but as Authlete is flexible enough we should consider other things

justin> re: self-contained, what I mean by that is that inevitably the token will be used to look up something in a database someplace

taka> Yes, I understand.

justin> :nod: ok. There is a design pressure, that I’ve seen, to put as much information into the JWT itself so as to minimize lookup and network calls. This is a dangerous pattern with often unintended consequences in security and privacy because you have an all-powerful artifact that leaks everywhere it’s used.

justin> We saw this happen with SAML

justin> Since OAuth is much more about API access we see it less, but it still can creep in

taka> So, Authlete can embed information equivalent to the userinfo property into a JWT-based access token but should dare not to do it. Thank you for your valuable comment.

justin> Yes, it would be technically feasible but I would not recommend it as either an implementation or a general pattern.


私が興味深いと感じたのは、“No JWT based system is fully self-contained” と “the fact that a field exists at all could be privacy leaking” です。つまり、「JWT ベースと言えども現実的には JWT だけで完結するシステムはない」、「標準クレームはまだしも、カスタムクレームについては JWT 内にクレームが存在するだけで個人情報流出になる恐れがある」、という点が興味深いと思いました。

こういう点を考慮すると、アクセストークンの形式として JWT を採用するにしても、結局対応するデータレコードをサーバー側のデータベースに持つハイブリッド型に寄っていくのではないかと思います。


5. Authlete の実装

Authlete が生成するアクセストークンは識別子型ですが、Authlete 2.1 以降、『アクセストークン署名アルゴリズム』を設定することにより JWT 形式のアクセストークンも発行できるようになりました。実装分類としてはハイブリッド型になります。

authlete_access_token_types.png

Authlete による JWT 形式アクセストークンの実装に関する留意事項は下記のとおりです。


  1. 署名アルゴリズムとして選択できるのは非対称鍵系のみ。「4.1.1. 対称鍵系署名アルゴリズム」で述べたとおり、認可サーバーとリソースサーバー間で共有する鍵の決め方について、標準仕様が存在しないため、対称鍵系アルゴリズムのサポートについては様子見としています。また、この判断は、2019 年 2 月に承認された CIBA Core 1.0 の『7.1.1. Signed Authentication Request』において、署名アルゴリズムを意図的に非対称鍵系のみに限定しているという事実の影響も受けています(=非対称鍵系に限定することはそれほど筋が悪いわけではないという判断)。


  2. 暗号化はサポートされない。「4.2. 暗号化」で述べたとおり、暗号鍵の扱いについて標準仕様が存在しないため、暗号化のサポートについては様子見としています。簡易的に暗号化を実装することは可能ですが、正しいアプローチは、リソースサーバーのメタデータおよび認可サーバー(群)とリソースサーバー(群)の関係性を適切に定義するところから始めるべきで、気軽に設計できるものではないという判断があります。


  3. 認可リクエストで要求されたクレーム群に関する情報を JWT 形式アクセストークンに埋め込まない。「コラム:潜在的な個人情報流出」で言及したように、個人情報流出の恐れがあるため、敢えて当該情報を含めない実装としています。なお、Authlete が発行する JWT 形式アクセストークンに含まれるクレームの種類については、authlete-java-common ライブラリの JavaDocService クラスの説明を参照してください。


  4. カスタムクレームをサポートする。Authlete には元々、任意のキー・バリューペアをアクセストークンに紐付ける仕組みがあります(参考:『アクセストークンと任意のデータ』)。そのようなペアのうち、hidden=false となっているものは、JWT 形式アクセストークンに埋め込まれます。なお、アクセストークンに埋め込まれる際、バリューは常に文字列として埋め込まれます。


Authlete 2.1 は、CIBA※8FAPI※9 をサポートする世界最先端の認可サーバー/OpenID プロバイダーの実装です。ご利用につきましては sales@authlete.com までお問い合わせください。

※8:参考→【2019年版】世界最先端の認証認可技術、実装者による『CIBA』解説

※9:参考→【2019年版】世界最先端の API セキュリティー技術、実装者による『FAPI(Financial-grade API)』解説