OAuth 2.0 / OIDC におけるAuthorization ServerからClientへのリクエストとその検証方法について
ritouです。
OAuth 2.0, OpenID Connectの実装の話です。良かったら読んでみてください。
いきなりまとめ
わかってる人向けに3行で説明します。
- Authorization Server から Client へのリクエストを送りたい事案が発生
- OAuth 2.0/OIDC関連の仕様の中で同様のことをしていないか調べた結果、RFC8417 Security Event Tokenを使うユースケースが存在
- 検証側の実装を考慮して、Client認証の
private_key_jwt
にAuthorization Serverの識別子と署名を適用して使うことにした
それでは始めます。
背景
OAuth 2.0(とOIDC)の登場人物は次のとおりです。
- Resource Owner (End User)
- Authorization Server (OpenID Provider)
- Client (Relying Party)
- Resource Server (UserInfo Endpoint)
このうち、ClientがAuthorization Serverに各種トークンの発行要求、無効化要求などでサーバー間の直接通信が行われることがあります。
Client Secretやクライアント証明書などを安全に管理できるConfidential Clientからリクエストを受けたAuthorization Serverは、送られてきたコンテンツを生成した、リクエストを送信したのが正当なClientであることを検証します。これがいわゆるClient認証です。
このClient認証はリクエストの主となるパラメータとは別のパラメータ、もしくはTLSのレイヤーで実現できることが特徴であり、実装をMiddlewareやPlugと呼ばれるものに有る程度切り出すといったこともできます。
今回はその逆、Authorization ServerからClient(あるいはResource Server)に対してサーバー間の直接通信を行うケースに注目します。
先日、"Authorization ServerからClientにリクエストを送りたいんや" という話が出てきました。
ちなみにそのリクエストはユーザーのアクションとは非同期で送られるイメージです。
標準化された仕様に囲まれている場合、次のような思考で考えていきます。
- 既に仕様になっているもので用途が一致するものがあればそれを使う
- そうじゃなかったら検討事項を増やさない範囲で既存の仕組みの応用を考える
- どうしようもなかったら独自で考える
たまに "調べたけど誰もやってないのは、そもそもやっちゃいけないことだった!" という場合もあるのでよく考えましょう。
まずは似たようなことをしている既存の仕様を見ていきます。
既存の方法
ある程度触ったことのある開発者であれば気付けるのですが、OAuth/OIDCのプロトコルにおいて、Authorization ServerからClientにサーバー間通信でリクエストを送るケースはあまり多くありません。
まずは OpenID Connect Client-Initiated Backchannel Authentication Flow - Core 1.0 の Ping/Push Callbackです。
CIBAはサービスが認証処理を行う端末とユーザーインタラクションを行う端末が別なケースに適用できる仕様です。ユーザーインタラクションが終わったタイミングで認可サーバーがクライアントにトークンを渡す方法がいくつか(Pull, Ping, Push)あります。この中でPing/Pushモードと呼ばれるフローではAuthorization ServerからClientに対してサーバー間通信のリクエストが送られます。
ここでは、それまでのやり取りの中でAuthorization Serverが発行した client_notification_token
と呼ばれるものが使われます。
OAuth 2.0の認可イベント、OIDCの認証イベントに対して一意に発生するようなケースでは有用でしょう。
次に、OpenID Connect Back-Channel Logout 1.0のBack-Channel Logout Requestです。
セッション管理のコンテキストでAuthorization Serverが「(このユーザーの)このセッションをログアウトさせたよ」というのをClientに通知する仕組みですが、ここではLogout Tokenと呼ばれるJWTが送られます。仕様にも記載されている通り、このJWTの内容はRFC8417 Security Event Tokenが元になっております。
同じくSETを使う仕組みとして、Shared Signals Framework(SSF)のRISCやCAEPなどもあります。例えばRISCではあるアカウントに対して状態変更があった際に連携済みのサービスに通知する仕組みです。
この辺りの仕様では、OAuth/OIDCのClient認証とは異なり、リクエストのコアなパラメータであるJWT自体がメタデータを含む感じです。
SETを扱うしくみであればこれにそのまま乗っておけば良さそうですが、今回の話はClient認証相当のものを逆方向でやりたいというところだったので、採用を見送りました。
Client認証の応用
ここからはClient認証の最小限のカスタマイズで実現することを考えます。
まず、Authorization Serverが持っているクレデンシャルから実現可能なClient認証の方式を整理します。
-
client_secret_basic
,client_secret_post
,client_secret_jwt
: Authorization Serverはserver_secret
的なものは持っていないがリクエストを送る先のClientに対するclient_secret
は知っているのでそれを使う?ちょっと気持ち悪いし漏洩なども怖い -
private_key_jwt
: OIDCのIDTokenで使っている鍵ペアを適用できそう - MTLS: クライアント証明書の用意、受信側の対応コストがあるのでなし
ということで、private_key_jwt
の採用を検討しました。
仕様を引用します。
iss
REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client.
sub
REQUIRED. Subject. This MUST contain the client_id of the OAuth Client.
aud
REQUIRED. Audience. The aud (audience) Claim. Value that identifies the Authorization Server as an intended audience. The Authorization Server MUST verify that it is an intended audience for the token. The Audience SHOULD be the URL of the Authorization Server's Token Endpoint.
jti
REQUIRED. JWT ID. A unique identifier for the token, which can be used to prevent reuse of the token. These tokens MUST only be used once, unless conditions for reuse were negotiated between the parties; any such negotiation is beyond the scope of this specification.
exp
REQUIRED. Expiration time on or after which the JWT MUST NOT be accepted for processing.
iat
OPTIONAL. Time at which the JWT was issued.
OIDCのIDTokenではJWTの iss
が Authorization Server の識別子、aud
は ClientID などが指定され、Authorization Serverによって生成されたJSON Web Signatureが採用されています。
Client認証における iss
, sub
の部分を Authorization Server に置き換え、aud
に ClientID を指定します。
ClientAssertionのJWTをデコードした値は次のようになります。
# Header
{
"alg": "ES256",
"kid": "1e9gdk7"
}
# Payload
{
"jti": "a73adceb-1df2-434c-9fae-a369706d8184",
"iss": "http://server.example.com",
"sub": "http://server.example.com",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"iat": 1311280970
}
Authorization Serverからのリクエストを受け取ったClientは署名検証をした上でPayloadの検証を行います。この手順も既にあるものが適用できるでしょう。
"Server認証としてパラメータも server_assertion
にする?" みたいな気持ちもちょっとありましたが、世の中のOAuth 2.0/OIDCのライブラリなどでClient認証の部分がそのまま使えるならその方がいいだろうということで、表に見えるパラメータとしてはClient認証のものと同じでいいかなと判断しました。
もう一度、まとめ
- Authorization Server から Client へのリクエストを送りたい事案が発生
- OAuth 2.0/OIDC関連の仕様の中で同様のことをしていないか調べた結果、RFC8417 Security Event Tokenを使うユースケースが存在
- 検証側の実装を考慮して、Client認証の
private_key_jwt
にAuthorization Serverの識別子と署名を適用して使うことにした
実装してみて問題があったら考え直しますが一旦こんな感じでやろうと思っています。
これを使う具体的なユースケースについても後々紹介できればと思います。
ではまた。
Discussion