OAuth 2.0/OpenID Connectの基本

最近、ほとんどのユーザはソーシャルプラットフォーム経由でのログインをすることが増えただろう。それを実現するために用いられている技術がOpenID Connectである。

OpenID ConnectはOAuth 2.0という認可の基盤の上に乗っかって、認証を行うための仕組みである。普通の人は、「認証と認可って何が違うの？」と質問するわけだが、この2つは根本的に異なる。

# 認証とは

認証というのは端的に言えば「私が誰であるか」を証明することである。だから、身分証のようなものと思ってもらってよい。

# 認可とは

認可というのは端的に言えば「あることをしても良い」という許可である。だから、合鍵のようなものと思ってもらってよい。極端な話、合鍵を発行するのに必ずしも認証は必要ではない。

# 認証と認可を取り違えるとどんな事故が起きるのか

認証を認可代わりにすると、何も権限チェックをしないわけだから、権限がない人であっても勝手にリソースを操作されてしまう。

逆に認可を認証代わりにすると、認可期限が切れるまでプログラムが認証中勝手にリソースを操作できてしまう。

# 具体的なフロー

OAuth 2.0とOpenID Connectは先述の通り、同一の基盤に基づいて行われるものである。登場するのは以下の4つである。

* リソース所有者 - プラットフォームにリソースを持っている人
* クライアント - リソース所有者の認証・認可をもらいたいアプリケーション
* 認可サーバ - プラットフォームで認証・認可を行うサーバ
* リソースサーバ - プラットフォームにあるリソースのサーバ

フローには以下の4種類が存在する。

* 認可コードフロー
    * クライアントからリソース所有者にコードをもらうように依頼し、コードをもらった上でクライアントが認可サーバからアクセストークンなどをもらうフロー
    * トークンの漏洩リスクは一番小さい
* 暗黙的フロー
    * クライアントがリソース所有者にアクセストークンなどをもらうように依頼し、アクセストークンなどをリソース所有者経由でもらうフロー
    * HTML+JavaScriptなど、クライアントサイド側で動くタイプのアプリケーションで用いる
    * こちらもユーザのID/パスワードは流れないが、アクセストークンをクライアントサイドで保持するため、漏洩リスクは上がる
* リソース所有者の認証情報フロー
    * リソース所有者の認証情報でクライアントが直接アクセストークンなどを取得するフロー
    * 極めてリスクが高いことから非推奨。他の2つのフローが取れない場合のみ
* クライアントの認証情報フロー
    * リソース所有者に関係なく、クライアント自身が保有している認証情報でリソースサーバにアクセスするフロー
    * リソース所有者とは関わらない、クライアント自身に関わる情報に用いる

このうち「クライアントの認証情報フロー」は今回の本質ではないし、「リソース所有者の認証情報フロー」もまず使われないことから割愛する。

## 認可コードフロー

認可コードのフローは以下の通り

1. クライアントはリソース所有者に認可サーバの認可URIを発行する
    * このURIには以下の情報が含まれる
        * 認可フローのタイプ(code)
        * クライアントのID
        * 認可/拒否後のリダイレクト先URI
        * 認可範囲
        * 状態情報
    * 状態情報に関しては、後でリクエスト検証のために用いる
2. 認可URIをもらったリソース所有者は、認可URIにアクセスする
3. 認可サーバはリソース所有者の認証をおこなった上で、認可範囲へのアクセスをクライアントに認めるか確認する
4. リソース所有者は、許可ないしは拒否の決定を行うと、リダイレクト先URIにその返答を渡す
    * 許可の場合、以下の内容が入ったパラメータを渡す
        * 認可コード
        * 状態情報
    * 拒否の場合、以下の内容が入ったパラメータを渡す
        * エラーの内容(これには拒否も含まれる)
        * 状態情報
5. クライアントは、状態情報をもとに、クライアントとしての認証のもと(例えばGoogleではクライアントシークレットを使う)以下の4つの情報を認可サーバに渡す
    * 認可タイプ(authorization_code)
    * 認可コード
    * リダイレクト先URI
    * クライアントID
6. 認可サーバは、これらの情報に問題がなければ、アクセストークンと、認可されていればリフレッシュトークンを発行する
7. クライアントは、アクセストークンを用いて実際にリソースにアクセスする

クライアントとしての認証が絡むので、ブラウザサイドで動くアプリケーションでこれを用いることができない。ただし非常にセキュアなので、6の時にリフレッシュトークンというものも認可があればもらうことが可能。このリフレッシュトークンはアクセストークンの有効期限が切れた後も、このリフレッシュトークンの有効期限が残っている限りにおいて、アクセストークンに引き換えが可能というものである。その際の流れは以下の通り。

1. クライアントは認可サーバに、クライアントとしての認証のもと以下の2つの情報を認可サーバに渡す
    * 認可タイプ(refresh_token)
    * リフレッシュトークン
2. 認可サーバは、これらの情報に問題がなければ、アクセストークンなどを発行する

## 暗黙的フロー

ブラウザで動くJavaScriptアプリケーションの場合、都度都度サーバにアクセストークンなどをもらっていては、そのトークン発行のためのAPIを書かなければならず、開発効率が悪い。このため、セキュリティを多少犠牲にしてでも簡便な方法が可能になっている。

1. クライアントはユーザーエージェント(要するにブラウザ)に認可サーバの認可URIを発行する
    * このURIには以下の情報が含まれる
        * 認可フローのタイプ(token)
        * クライアントのID
        * 認可/拒否後のリダイレクト先URI
        * 認可範囲
        * 状態情報
    * 状態情報に関しては、後でリクエスト検証のために用いる
2. ユーザーエージェントは上記URIへリダイレクトする
3. 認可サーバはリソース所有者の認証をおこなった上で、認可範囲へのアクセスをクライアントに認めるか確認する
4. リソース所有者は、許可ないしは拒否の決定を行うと、リダイレクト先URIにその返答を渡す
    * 許可の場合、以下の内容が入ったパラメータを渡す
        * アクセストークン
        * トークンタイプ(典型的にはbearerが入るが、macと入ったりすることも)
        * 有効時間の秒数(例えば3600とあったら1時間)
        * 認可範囲
        * 状態情報
    * 拒否の場合、以下の内容が入ったパラメータを渡す
        * エラーの内容(これには拒否も含まれる)
        * 状態情報
5. ユーザーエージェントはリダイレクト先のURIを開き、スクリプトを実行し、クライアントに4の情報を渡す

トークンが直接URIとして渡されることから、ユーザがトークンに触れる機会が発生する。このため、リフレッシュトークンは発行してはならないと定められている。有効時間が切れたら、再びアクセストークンの取得からやり直しである。ただし、認可自体はすでに終わっているので、認可画面は出さずに直ちにリダイレクトURIへリダイレクトし、アクセストークンをすぐに取得できる。

# OpenID Connect

ここまでOAuth 2.0の基本的なフローを見てきたが、OpenID Connectではどのような変更が加えられているのか。

* 共通事項として、scopeにはopenidと含める必要がある
* 認可コードフローの場合、手続き6(コードからアクセストークンに引き換える応答)の際、アクセストークンとともにIDトークンが発行される
* 暗黙的フローの場合、以下のような変更がある
    * 1で指定する認可フローのタイプはid_token単独もしくはtoken id_tokenの2つ指定のどちらか
    * 4のリダイレクト先URIのパラメータは、id_token単独ならアクセストークンはなくIDトークンのみ、token id_tokenの2つ指定の場合はアクセストークンとIDトークンの両方を含む

IDトークンは、JWT(JSONウェブトークン)という形式であり、以下の3つの要素から構成され、その間はピリオドで区切られBase64で符号化される(ただし、0x3Eは-、0x3Fは_で符号化され、パディングの=はトリミングされる)。

* JOSEヘッダー。署名のアルゴリズムや、署名の鍵にかかわる情報が入っている
* クレーム。簡単に言えばキーと値のペアである。最低限、以下の情報が入っている必要がある
    * iss - トークンの発行者。例えばGoogleであれば「 https://accounts.google.com 」もしくは「accounts.google.com」である
    * sub - ユーザを一意に識別する識別子。255文字以下で大文字と小文字は区別される
    * aud - トークンの発行先。OAuth 2.0のクライアントID
    * exp - トークンの有効期限。The Epoch(UTCで1970年1月1日午前0時)からの経過秒数(例えば日本時間で2023年2月9日午前10時であれば1675904400という数値になる)
    * iat - トークンの発行日時。The Epochからの経過秒数
* 署名 - トークンが確かに発行者から発行されたことを証明するための署名

通常、検証のフローは以下の通りである。

1. ピリオドでコンポーネントを分解し、上記3つのコンポーネントに分ける
2. JOSEヘッダーをBase64デコードし、理解可能な形式かどうかを確認する。以後、このトークンはJWSである前提で話をする
3. クレームを検証なしでデコードする
4. JOSEヘッダーとクレームのissから、検証のために必要な公開鍵を入手する
5. トークンの署名が正しいかどうかを検証する
6. 署名が正しければ、クレームの以下の情報が正しいかを検証する
    * issが意図した発行元であるか
    * audがクライアント自身のクライアントIDと一致するか
    * expで指定した有効期限が到来してないか

OpenID Connectでは、以下のクレームが標準で定義されている。

* name - フルネーム
* given_name - 日本でいう下の名前
* family_name - 日本でいう苗字
* middle_name - ミドルネーム
* nickname - ニックネーム
* preferred_username - ユーザの使いたい呼び名
* profile - プロフィールページのURL
* picture - プロフィール画像
* website - ユーザのWebサイトURL
* email - メールアドレス
* email_verified - メールアドレスが認証済みか否か(bool)
* gender - 性別。ただしこれは生物学的な性ではない。male(男性)・female(女性)が定義されているが、これ以外であってもよい
* birthdate - 生年月日(YYYY-MM-DD形式)。YYYYが0000の場合、それは入力されていないことを示す。YYYYのみ指定することも可能
* zoneinfo - タイムゾーン。例えば日本標準時であればAsia/Tokyoと指定する
* locale - ロケール。BCP47で定められた形式を用いる。例えば英語(アメリカ)であればen-USと指定する
* phone_number - 電話番号
* phone_number_verified - 電話番号が認証済みか否か(bool)
* address - 住所(以下のキーを含んだオブジェクト)
    * formatted - 人間が読める形の全文の住所
    * street_address - 町名番地
    * locality - 市や地区など
    * region - 州・都道府県・地方など
    * postal_code - 郵便番号
    * country - 国
* updated_at - 更新日時(The Epochからの秒数の数値)

当然、他の属性をプラットフォームが追加で定義してもよい(例えばGoogleではhdという属性を定義しており、Google Cloudの組織ドメインが入っている。これにより、特定組織以外のGoogleアカウントでのログインを阻止することができる。また、Amazon Cognitoユーザープールではcognito:という接頭辞のついた属性が独自属性である(cognito:usernameはユーザ名。それ以外にもユーザープールごとの拡張属性が定義されている))。

# 詳細な情報

詳細な情報は、以下から取得可能である

* https://www.rfc-editor.org/rfc/rfc6749
* https://openid.net/specs/openid-connect-core-1_0.html