Oracle Cloud Hangout Cafe Season4 #4「マイクロサービスの認証・認可とJWT」(2021年7月7日開催)
クライアントタイプ
OAuth 2.0では、クライアント(=リソースサーバーに対して要求を行うアプリケーション)を以下のように分類しています。
- コンフィデンシャルクライアント
- クライアントシークレットを安全に保持できるクライアントを指す
- 典型的には、サーバーサイドの Web アプリケーションとなる
- パブリッククライアント
- クライアントシークレットを安全に保持できないクライアントを指す
- 典型的には、ネイティブアプリやフロントエンドの JavaScript アプリケーションとなる
グラントタイプ
OAuth 2.0では、アクセストークンの取得方法を4つ定義しています。そして、このアクセストークンの取得方法をグラントフローと呼びます。定義されているグラントフローと簡易的な特徴は以下の通りです。
- 認可コードグラント
- OAuth 2.0 の基本フロー
- 短命な認可コードを用いて、アクセストークンと交換を行うフロー
- インプリシットグラント
- 認可コードを用いずに、直接アクセストークンを受け取るフロー
- SPA(Single Page Application)などを想定したフローだが、現在は非推奨であり、認可コードグラント + PKCE(Proof Key for Code Exchange by OAuth Public Clients)が推奨されている
- リソースオーナーパスワードクレデンシャルグラント
- リソースオーナーの ID/Password を受け取り、アクセストークンを発行するフロー
- クライアントクレデンシャルグラント
- クライアント認証のみで、アクセストークンを発行するフロー
- クライアントとリソースオーナーが同一の場合に使用される
以降では、基本フローである認可コードグラントのみを説明します(その他のフローは、認可コードグラントから引き算して考えると分かりやすいと思います)。
認可コードグラントによるアクセストークンの取得フロー
下図が認可コードグラントの全体フローになります。
ポイントとなる箇所を見ていきましょう。まずは、図中の認可リクエストについてです。認可サーバーから提供されている認可エンドポイントに対して、クライアントがリダイレクトを利用してリソースオーナーを導きます。この時のリクエストは典型的に以下のようになります。
GET /authorize // 認可エンドポイント ?client_id=2b0c89ed177d42858d48747f97f03b56 // OAuth 2.0 のクライアント ID &response_type=code // OAuth 2.0 のグラントタイプを指定 (認可コードグラント) &redirect_uri=https://client.example.com/cb // クライアントへのリダイレクト URL &scope=get_list view_photo // 要求するリソースのスコープ &state=b621b8e1-eeff-487c-9ef3-ab92a178a720 // state(セッション単位で使うランダムな値で、CSRF を防ぐために利用すべき)
図中の認証画面〜認証情報入力では、認可サーバーに対してリソースオーナーであることの認証が行なわれます(i.e: リソースサーバーに対するアクセス権を許可できる人物なのか? ということの認証)。
そして、権限委譲の確認画面〜権限委譲の同意では、クライアントがリソースサーバーに対して要求するスコープの確認とその同意が行なわれます(様々な場面で以下のような画面を見たことがあると思います)。
クライアントの資格情報や権限委譲の同意が完了すると、図中の認可レスポンス(認可コード)〜トークンリクエストのように、認可サーバーは認可リクエスト時に送られてきたリダイレクトURIに対して、リソースオーナーをリダイレクトさせます。このときの認可レスポンスは以下のようになります。
HTTP /1.1 302 Found Location: https://client.example.com/cb // クライアントへのリダイレクトURL ?code=AQIDBAWaII5T42Sle5s63NpsgFkEt8GzJI… // 認可コード &state=b621b8e1-eeff-487c-9ef3-ab92a178a720 // state (セッション単位で使うランダムな値で、CSRFを防ぐために利用すべき)
この中に出てくるcode=AQIDBAWaII5T42Sle5s63NpsgFkEt8GzJI…
が認可コードです。認可コードとは、クライアントがリソースオーナー(ブラウザ)を介さずにアクセストークンを取得するために用いる一時コードのことで、RFC 6749では有効期限を10分以内にすることが推奨されています。
そして、図中のトークンリクエストのように、クライアントから認可サーバーのトークンエンドポイントに対して、認可コードとアクセストークンを交換するためのリクエストが発行されます。このときの形式は典型的に以下のようになります。
POST /token HTTP /1.1 // トークンエンドポイント Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW… // クライアントの認可サーバーに対する認証(Basic認証) Content-Type: application/x-www-form-urlencoded grant_type=authorization_code // OAuth 2.0 のグラントタイプを指定 (authorization_codeで固定) &code=AQIDBAWaII5T42Sle5s63NpsgFkEt8GzJI // 取得した認可コード &redirect_uri=https://client.example.com/cb // クライアントへのリダイレクトURL
ここでは、クライアントがアクセストークンを所持しても良いのか? という確認のために認可サーバーに対するクライアントの認証がクライアントID/Secretを用いたBasic認証にて行なわれ、認証後にリクエストに含まれている認可コードとアクセストークンの交換が行われます。クライアントへは、図中のトークンレスポンス(アクセストークン)のようにトークンとそれに関連する情報(有効期限やトークンのタイプ)が返却されます。具体的には、以下のような情報が返却されます。
HTTP /1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { “access_token”: “eyJ4NXQjUzI1NiI6IjdZalN6SVl3SnRb3oKpEH9h6iqjpptaNnLsEG…”, // アクセストークン “token_type”: “Bearer”, //クライアントからのリソースアクセスの際のアクセストークンの使い方 “expires_in”: 3600, // アクセストークンの有効期限 “refresh_token”: “AQIDBAX6uRDWMb3oKpEH9h6iqjpptaNnLsEG_Qr4 xB7wYQrf…” // リフレッシュトークン }
このとき、認可サーバーの実装や設定によってはリフレッシュトークンという現在発行されているアクセストークンが無効化、もしくは期限切れとなった際に、新しいアクセストークンを取得するためのコードが含まれる場合もあります。
最後に取得したアクセストークンをリクエストに含め、リソースサーバーにアクセスを行います。この際、リソースサーバー側でアクセストークンの検証が行われます。具体的には「アクセストークンの発行先が誤っていないか」「期待通りの発行元から発行されたアクセストークンなのか」「アクセストークンが有効期限内なのか」「エンドポイントを実行するために十分なスコープを有しているのか」といった検証が行われ、検証の後にリソースアクセスが許可されます。
【OAuth 2.0まとめ】
- OAuth 2.0 は権限委譲/認可を扱うプロトコルであるRFC 6749にて規定されている
- まずは、基本のフローである認可コードグラントを理解し、その他のフローは引き算で覚える
- 誤った使い方/実装をしてしまうと、アプリケーションに脆弱性を埋め込むこととなる
- (工夫なしに)エンドユーザーの認証目的に使う、など
- エンドユーザーの認証が目的であれば、後述する OpenID Connect を使用すること
OpenID Connect 1.0
以降では、OpenID Connect 1.0について、主にIDトークンに焦点を当てて説明します。
OAuthとOpenID Connectの関係
まずは、前述したOAuth 2.0とOpenID Connect 1.0の関係を整理します。OpenID Connect Core 1.0 incorporating errata set 1のAbstractに以下のように記載がされています。
OpenID Connect 1.0は、OAuth 2.0プロトコルの上にシンプルなアイデンティティレイヤーを付与したものである。このプロトコルはClientがAuthorization Serverの認証結果に基づいてEnd-Userのアイデンティティを検証可能にする。また同時にEnd-Userの必要最低限のプロフィール情報を、相互運用可能かつRESTfulな形で取得することも可能にする。
つまり、OpenID Connect 1.0とは、OAuth 2.0を認証目的で使うためにIDトークンとProfile APIを追加し拡張したものと言い換えることができるでしょう。
OpenID Connect 1.0のアクター
OAuth 2.0と同様に、まずはアクターを整理します。下図もOAuth 2.0の概要で紹介した例を一般的にしたものです。
各名称と役割は以下に記します。XXX(YYY)のように表記した場合は、XXX: OpenID Connect 1.0 のアクター、YYY: OAuth 2.0 のアクターとします。
- エンドユーザー(リソースオーナー)
- OIDC には、リソースの概念が存在しないため単に認証対象のユーザーをエンドユーザーと呼びます
- リライング・パーティー(クライアント)
- ID プロバイダが認証した結果を受け取るアプリケーションを指します
- 認証した結果(ID トークン)を検証することでエンドユーザーを信頼(Rely)し認証連携を行うことから、リライング・パーティーと呼ばれています
- UserInfo エンドポイント(リソースサーバー)
- ユーザーの情報を提供する API エンドポイント
- OpenID プロバイダ(認可サーバー)
- OP などと略称を見かけた際は、こちらのことを指しています
- 主に、ID トークンおよびにアクセストークンを発行する役割を持ちます
IDトークン
IDトークンについて整理します。IDトークンはエンドユーザーの認証に必要な項目(Claim)を含んだセキュリティトークンとなっており、「誰が? 誰を? 誰のために?」などの認証イベントの情報を含むように標準化されています。トークンの形式は、前述したJWS形式のJWTやNested JWT(JWSをJWRが含む形式)で表現されます。また、JWT形式のアクセストークンとの大きな違いとしては、発行先が違うことが挙げられるでしょう。
上図は、JWT形式のアクセストークンのペイロード例とIDトークンのペイロード例のとなりますが、見比べてみると以下の違いを確認できます。
- アクセストークン → リソースサーバーに向けて発行
- ID トークン → リライング・パーティー(OAuth クライアント)に向けて発行
ペイロード内の標準クレーム
下表は、IDトークンのペイロード内の標準クレームとなります(*: 必須項目)。
claim | description |
---|---|
* iss | Issue Identifier: ID Token の発行者 |
* sub | Subject Identifier: End-User の識別子 |
* aud | Audience: ID Token の発行対象で、OAuth 2.0 の client_id を含む必要がある |
* exp | Expiration Time: ID Token の有効期限 |
* iat | Issuer At: JWT の発行時刻 |
auth_time | End-User の認証が発生した時刻。リクエストに max_age が含まれている場合は必須 |
nonce | Client セッションと ID Token を紐づける文字列。リプレイ攻撃防止用のパラメータ |
acr | Authentication Context Class Reference: 実施された認証処理が満たすAuthentication Context Classを示す |
amr | Authentication Method References: 認証方法を示す |
azp | Authorizated Party: ID Token の発行対象である認可された関係者(=OAuth 2.0 の client_id) |
これを読み解くと、「IDトークンはiss(=OpenID プロバイダ)がsub(=エンドユーザー)をaud(=リライング・パーティ)のために認証しました」という情報が含まれたトークンであると言えます。それゆえ、リライング・パーティーはこのIDトークンを適切に検証することでOPが認証した結果を信頼し、エンドユーザーの認証連携を行うわけです。