OAuth

読み:おーおーす / 英語表記:OAuth

利用者のパスワードを渡さずに、第三者のアプリケーションへ限られた範囲のアクセス権限を委ねるための認可の枠組みである。アプリケーションは利用者の同意を経て発行されたアクセストークンを提示し、APIなどの保護された資源を利用する。

イマシルちゃんの解説

クラウドに保存した写真を、印刷サービスのアプリに読ませたい場面を考えてみよう。
アプリにクラウドのパスワードを教えると、写真の削除や設定の変更までできてしまう場合があるんだ。
OAuthは、パスワードを教えずに限られた権限をアプリに任せるための認可の枠組みだよ。
利用者がクラウド側でログインして同意すると、アプリは許された操作を示すアクセストークンという文字列を受け取るの。
アプリはこれを提示して「写真を読むだけ」のような許可された操作を行うんだ。
パスワードを預けずに必要な機能だけを連携できるのが便利だね。
ただしOAuth自体は、利用者が誰かというログイン結果をアプリへ伝えるための規格ではないよ。

より深い解説

定義と位置付け

OAuthは、HTTPで提供される資源へのアクセス権限を、資源の持ち主に代わって第三者のアプリケーションへ与えるための仕様である。現在広く使われるOAuth 2.0はRFC 6749で定められ、OAuth 1.0(RFC 5849)を置き換えた。2.0は1.0と互換性がない。権限を表す情報をHTTPで送る方法はRFC 6750が定める。

OAuthが解決するのは、利用者のパスワードを第三者に預ける運用の問題である。パスワードを渡すと、アプリケーションは利用者と同じすべての操作ができる。特定のアプリケーションだけを止めるには、パスワードを変えるしかない。アプリケーションからパスワードが漏れれば、ほかのサービスにも被害が広がり得る。OAuthでは、範囲と期限を絞った権限だけを渡し、アプリケーションごとに取り消せるようにする。

ここで扱うのは「この相手に何を許すか」を決める認可であり、「相手が誰か」を確かめる認証とは別の仕事である。OAuthの仕組みの中でも利用者のログインは行われるが、その方法は仕様の範囲外であり、ログイン結果をアプリケーションへ伝える形式も定めていない。この区別は後の節で詳しく扱う。

四つのロール

OAuthでは、登場者を役割(ロール)で区別する。写真の印刷サービスの例に当てはめると次のようになる。

ロール役割写真の例
リソースオーナー資源の持ち主。アクセスを許すかどうかを決める写真を保存している利用者
クライアント利用者の代わりに資源へアクセスするアプリケーション印刷サービス
認可サーバ利用者を確認して同意を得て、トークンを発行するクラウド事業者のログイン・同意画面を持つサーバ
リソースサーバ資源を保持し、トークンを確認してAPIを提供する写真を返すAPIサーバ

ここでの「クライアント」は利用者の端末のことではなく、権限を受け取るアプリケーションを指す。Webサーバ上で動くアプリケーションもクライアントである。トークンを発行するサーバとAPIを提供するサーバは同じ事業者が運用することが多いが、別々のサーバとしてもよい。

クライアントは、事前にトークンを発行するサーバへ登録してクライアントIDを受け取る。登録時には、処理結果を受け取るURL(リダイレクトURI)も届け出る。

コードを経由してトークンを受け取る流れ

Webアプリケーションで最も基本となる手順が認可コードグラント(Authorization Code Grant)である。グラントとは、クライアントが権限を得るための手順の種類を指す。

  1. 利用者が印刷サービスで「クラウドから写真を読み込む」を選ぶ。
  2. 印刷サービスは、ブラウザを認可サーバの認可エンドポイントへリダイレクトする。このとき欲しい権限の範囲などをURLのパラメータに付ける。
  3. 認可サーバは利用者にログインさせ、印刷サービスへ渡す権限の内容を示して同意を求める。
  4. 同意されると、認可サーバはブラウザを登録済みのリダイレクトURIへ戻す。URLには使い捨ての認可コードが付く。
  5. 印刷サービスのサーバは、受け取ったコードを認可サーバのトークンエンドポイントへ直接送る。このときクライアントとして認証を受ける。
  6. 認可サーバはコードと送信元のクライアントを確かめ、アクセストークンを返す。
  7. 印刷サービスはアクセストークンを付けてリソースサーバのAPIを呼び出し、写真を受け取る。

手順2と4はブラウザのリダイレクトを経由する。この経路はフロントチャネルと呼ばれ、URLが履歴やログに残るなど漏れる機会が多い。手順5と6はクライアントのサーバと認可サーバが直接通信するバックチャネルである。ブラウザを通るのは短命で一度しか使えないコードだけにし、本当に使う権限はバックチャネルで受け取る。これが二段階に分ける理由である。

RFC 6749は、このコードの有効期間を最大10分にすることを推奨している。コードは一度しか使えず、同じコードが再び使われた場合、認可サーバは要求を拒否する。可能ならそのコードから発行済みのトークンも無効にするべきとされている。

要求と応答の例

次は手順2の要求の例である。値はすべて説明用で、実際には1行のURLとして送る。

GET /authorize?response_type=code
  &client_id=photo-print
  &redirect_uri=https://print.example/callback
  &scope=photos.read
  &state=k8Zq2v...
  &code_challenge=E9Mel...
  &code_challenge_method=S256
Host: auth.cloud.example

response_type=code が認可コードを求めることを示す。パラメータ内のURLは、実際にはパーセントエンコードして送る。state と code_challenge の役割は後の節で説明する。

手順5ではPOSTでトークンエンドポイントへ送る。grant_type には authorization_code を指定する。

POST /token
Host: auth.cloud.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlO...&redirect_uri=https://print.example/callback&code_verifier=dBjft...

成功するとJSONの応答が返る。トークンを含む応答には、キャッシュさせないためのCache-Control: no-storeが付く。

{
  "access_token": "2YotnF...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3J...",
  "scope": "photos.read"
}

access_token と token_type は必須である。expires_in は秒単位の有効期間で、この例の3,600秒は1時間に当たる。

権限を表す文字列とその範囲

アクセストークンは、クライアントに与えた権限を表す文字列である。権限の範囲はスコープで表す。この値は認可サーバが定める文字列を空白で区切って並べたもので、大文字と小文字を区別する。photos.read のような名前を付けるのは認可サーバ側の設計であり、仕様が名前を決めているわけではない。

クライアントは必要最小限の範囲だけを要求するべきである。認可サーバは、要求より狭い範囲でトークンを発行することもある。その場合は応答の scope に実際の範囲を示す。

多くの場合、アクセストークンはベアラートークンとして扱われる。これは「持っている者なら誰でも使える」トークンで、使う者が鍵を持っていることなどは確かめない。そのため盗まれればそのまま悪用され得る。RFC 6750はトークンを送る通信にTLSを必須とし、送り方として次の三つを挙げている。

送り方例扱い
AuthorizationヘッダーBearer 方式でトークンを付ける推奨。リソースサーバは必ず対応する
フォームの本文access_token=2YotnF...ヘッダーを使えない場合に限る
URLのクエリ?access_token=2YotnF...ログに残りやすく、現在は使ってはならない

HTTPのリクエストヘッダーに付けるのが基本である。URLに入れる方法は、RFC 9700で禁止された。

トークンの中身の形式はOAuth 2.0では決められていない。リソースサーバにとって意味を持たない乱数の文字列にして、認可サーバへ問い合わせて確かめる方式がある。この問い合わせはトークンイントロスペクション(RFC 7662)と呼ぶ。署名付きのJWTにして、リソースサーバが署名と有効期限を自分で検証する方式もある(RFC 9068)。前者は取り消しをすぐ反映しやすく、後者は問い合わせなしで検証できる。

期限切れへの備え

アクセストークンは短い期限にしておく。漏れたときの被害期間を短くするためである。期限が切れるたびに利用者へ同意を求め直すと不便なので、新しいアクセストークンを受け取るためのリフレッシュトークンを一緒に発行することがある。

この更新用のトークンは認可サーバへだけ送り、リソースサーバには送らない。アクセストークンより長く使えるため、漏えい時の影響は大きい。RFC 9700は、ブラウザやスマートフォンのアプリのように秘密を守れないクライアントに対して、このトークンを送信者に結び付けるか、使うたびに新しいものへ交換するローテーションを求めている。交換する方式では、古いトークンが再び使われたことから盗用を検知できる。

グラントタイプと現在の推奨

RFC 6749は四つのグラントタイプを定めた。その後の運用経験から、現在はすべてを同じように使うわけではない。

グラントタイプ概要現在の扱い
認可コードブラウザで同意を得て、コードをトークンに交換する基本の方式。PKCEと組み合わせる
インプリシット認可エンドポイントからトークンを直接受け取るトークンがURLで漏れやすく、使うべきでない
リソースオーナーパスワードクレデンシャルズ利用者のIDとパスワードをクライアントが受け取って送るパスワードを預ける問題が残るため、使ってはならない
クライアントクレデンシャルズクライアント自身の認証情報だけでトークンを得る利用者がいないサーバ間の連携に使う

この方式は、利用者の代わりではなく、クライアント自身の権限でAPIを呼ぶ場面に使う。夜間のバッチ処理が社内APIを呼ぶ例が当てはまる。このときは更新用のトークンを発行するべきでないとされている。

テレビのように文字入力が難しい機器では、別の端末で同意を行うデバイス認可グラント(RFC 8628)も使われる。

秘密を守れないクライアントを守る拡張

RFC 6749は、クライアントを二種類に分ける。サーバ上で動き、クライアントシークレットなどの認証情報を秘密にできるものがコンフィデンシャルクライアントである。ブラウザ上のJavaScriptやスマートフォンのアプリのように、配布物から秘密を取り出され得るものをパブリッククライアントという。

このようなクライアントは、トークンエンドポイントで自分を強く証明できない。スマートフォンでは、同じリダイレクト先を名乗る悪意のアプリが認可コードを横取りする攻撃も知られている。これを防ぐ拡張がPKCE(Proof Key for Code Exchange、RFC 7636)である。

  1. クライアントは要求ごとに、推測できない乱数の文字列を作る。これをcode_verifierと呼び、長さは43〜128文字である。
  2. その値をSHA-256でハッシュし、base64urlで符号化した値を code_challenge として認可要求に付ける。
  3. トークン要求では、元の値そのものを送る。
  4. 認可サーバは、送られた値から同じ計算をして、最初に受け取った値と一致するかを確かめる。

認可コードだけを盗んだ攻撃者は、元の乱数を知らないためトークンに交換できない。ハッシュを使う方式は code_challenge_method にS256と指定する。元の値をそのまま送る plain 方式は、ハッシュ方式を使えない場合に限られる。RFC 9700は、秘密を守れないクライアントにこの拡張を必須とし、コンフィデンシャルクライアントにも推奨している。

主な攻撃と対策

OAuthの攻撃は、リダイレクトでブラウザを行き来する部分と、トークンが漏れる部分に集中する。RFC 9700(OAuth 2.0のセキュリティのベストプラクティス)は次の対策を求めている。

攻撃・問題何が起きるか主な対策
CSRF攻撃者の認可コードを被害者のブラウザに受け取らせ、攻撃者の資源に結び付けるPKCE、または一度だけ使うstateをブラウザに結び付けて確認する
リダイレクトURIの改ざんコードやトークンを攻撃者のサイトへ送らせる登録したURIとの完全一致で比較する
認可コードの横取り盗んだコードをトークンに交換するPKCE
トークンの漏えいURLやログ、通信の盗聴から取り出されるTLS、URLに入れない、短い有効期限
盗んだトークンの再利用ほかのサーバや別の端末で使われる対象サーバの限定、送信者への結び付け

この値は、クライアントが認可要求に付けて送り、戻ってきた値が自分の送ったものと同じかを確かめるための値である。リダイレクトURIを前方一致などで比較すると、登録したURIの下に攻撃者が用意したページへ誘導される余地が生まれる。

送信者への結び付けは、トークンを鍵と結び付け、鍵を持つ者だけが使えるようにする考え方である。TLSのクライアント証明書を使う方式(RFC 8705)と、アプリケーション層で署名を付けるDPoP(RFC 9449)がある。これらを使うと、トークンを持つだけでは使えなくなる。

本人確認との違い

OAuthで受け取るアクセストークンは「このクライアントに写真を読む権限がある」ことを表す。「今アクセスしているのが誰か」をクライアントに証明するものではない。トークンの中身の形式も宛先もクライアント向けに定められていないため、クライアントはトークンを見て利用者を確かめられない。

この区別を無視して「アクセストークンを受け取れたらログイン成功」とする実装は危険である。別のアプリケーション向けに発行されたトークンを持ち込まれると、そのトークンの持ち主としてログインさせてしまうおそれがある。ログインに使うには、どのクライアントに向けて、いつ、誰が認証されたかを示す情報が要る。それを標準化したのがOpenID Connectである。

観点OAuth 2.0OpenID ConnectSAML
主な目的APIへのアクセス権限の委譲ログイン(認証結果の連携)ログイン結果や属性の連携
クライアントが受け取るものアクセストークンIDトークンとアクセストークンXMLのアサーション
主なデータ形式規定なし(応答はJSON)JWTXML
典型的な用途アプリからAPIを呼ぶ外部のアカウントでログインする企業のシングルサインオン

「OAuthでログインする」と呼ばれる機能の多くは、実際にはOpenID ConnectのようにOAuthの上へ認証の仕組みを加えたものである。

設計と運用

認可サーバ、クライアント、リソースサーバのそれぞれで確認する点が異なる。

担当設計・運用で確認すること
認可サーバリダイレクトURIの完全一致、PKCEへの対応、コードの使い捨て、同意画面での権限の表示
クライアント必要最小限のスコープ、state・PKCEの検証、トークンの安全な保存、URLに入れない
リソースサーバトークンの有効性・期限・対象・スコープの確認、TLSの必須化
共通通信はTLSで保護し、トークンをログへ出力しない

利用者が権限を取り消せる画面を用意し、取り消した結果がリソースサーバに反映されるかも確かめる。JWT形式のトークンを自前で検証する構成では、取り消しがすぐに効かない点を考慮して有効期限を短くする。

障害の切り分け

エラーは、どのエンドポイントで、どの段階で返ったかで原因を分ける。

現象考えられる原因
認可画面へ進まずにエラーになるクライアントIDの誤り、登録と異なるリダイレクトURI
トークン要求で invalid_grant が返る認可コードの期限切れ、再使用、リダイレクトURIやcode_verifierの不一致
APIが401と invalid_token を返すアクセストークンの期限切れや取り消し。リフレッシュトークンで取り直す
APIが403と insufficient_scope を返すトークンは有効だが、その操作に必要なスコープがない
JWT形式のトークンが発行直後から拒否されるサーバ間の時刻のずれ、署名検証用の鍵の不一致

401と403の区別は重要である。401はトークンそのものが使えない状態で、403は権限の範囲が足りない状態である。後者はトークンを取り直すだけでは解決せず、同意するスコープを見直す必要がある。エラーの種類と状態コードの対応はRFC 6750で定められている。

午後での問われ方

参考資料

この用語が出てくる記事

↑