OIDC に入門する
こちらの本に記載されている以外の事柄についても派生して諸々記載してある
OIDC とは
OIDC を使うと IdP が提供するアカウントでサードパーティアプリにログインすることが可能となる。OIDC によってアプリ、ユーザーともに余計なアカウントの ID / password を管理する必要がなくなり、さらにアプリとしては IdP が提供する属性情報を利用して新規登録時のプロフィール情報の入力、メールアドレスの到達確認を省略できるというメリットもある。
OIDC とは OAuth に ID トークンと UserInfo エンドポイントを加えたものと言える。ID トークンはユーザーを認証するために使用し、UserInfo エンドポイントはユーザーのプロフィール情報を得るために使用する。
OIDC のスコープ
openid
OIDC のリクエストであることを示す
profile
名前、誕生日、性別、写真などのプロフィール情報へのアクセス
email
email 及び email_verified へのアクセス
address
address へのアクセス
phone
phone_nuber 及び phone_number_verified へのアクセス
OAuth と OIDC の本質的な違いは ID トークンを発行の有無と言っても過言ではない。
ID トークン
ID トークンは誰が何のために使うのか
ID トークンを使うのは RP。エンドユーザーを認証するために使用
ID トークンは誰が発行するのか
IdP
ID トークンをいつ取得するのか
認可コードフロー、インプリシットフロー共にアクセストークンを取得するタイミングで同時に取得する。ハイブリッドフローでは response_type の指定によります。
ID トークンの形式は
JWT。RP は JWT をデコードすることで中から必要な情報を取り出す
ID トークンにはどのような情報が含まれているのか
ユーザー ID(sub)
エンドユーザーを識別するための ID
ID トークンの有効性を示す情報
有効期限や発行日時など
ID トークンのやり取りに関する情報
トークン発行者 ID、トークン受領者 ID
JWT に関する情報
トークンの形式、署名方式
ID トークンの署名
改ざんを防ぐための署名
ID トークンによる認証は OAuth 認証と比較して何が嬉しいのか
OAtuth 認証ではアプリの実装が不適切な場合アクセストークンの入れ替えによって他人としてログインできてしまったが、OIDC では RP が ID トークンに含まれる情報を適切に検証することで ID トークンの入れ替えによる攻撃を防ぐ。
JWT
JWT は、
ヘッダー
ペイロード
署名
の3つのパートで構成されており、それぞれのパートは . で区切られている
ヘッダー、ペイロード部分は Base64 URL エンコードされた jsonで、デコードすることで中身の json を見ることができる
ヘッダー
ヘッダーをデコードすると以下のような json が入っている
code:header.json
{
"typ": "JWT",
"alg": "RS256"
}
typ クレームはこれが JWT であることを示している。alg クレームは署名に使用されているアルゴリズムを表している。JWT の仕様では署名なしを表す none が値として定義されているが、OIDC の仕様では ID トークンとして none は禁止されている。alg が none の場合はエラーを返すべき。また、共通鍵を利用した HS256 (共通鍵暗号 HMAC 、ハッシュアルゴリズム SHA256) も JWT の仕様に定義されているが、クライアントシークレットを共通鍵として鍵として利用するために変更が難しかったり、パブリッククライアントでは利用できないといった制約もある。したがって、OIDC では公開鍵を利用した RS256 (公開鍵暗号 RSA 、ハッシュアルゴリズム SHA256)が一般的。
RS256 を利用した場合は公開鍵の ID として kid クレームがヘッダー部に入ることもある。
code:header.json
{
"typ": "JWT",
"alg": "RS256",
"kid": "aof9apy8hsdfp9quewiofooashf9uasf"
}
ペイロード
ペイロードは以下の形式の json を Base64 URL エンコードしたもの。ペイロードの代表的なものを下記に示す。仕様で定義されていないクレームについても仕様可能。
code:payload.json
{
"aud": "4567898765asidf.apps.googleusercontent.com",
"sub": "o-09u8y7trcfgbk",
"iat": 1672745601,
"exp": 1672749201,
"nonce": "56789-098765-987654"
}
iss
ISSuer の略。ID トークンの発行者を指す。URL の形式で表現され、通常は IdP の URL となる。
aud
AUDience の略。ID トークンの発行を受ける RP のクライアント ID が入る。通常は文字列の配列が入るが、RP が1つだけの場合のは単一の文字列でも良いことになっている
sub
SUBject の略。エンドユーザーの識別子。RP はこの値を持ってエンドユーザーを識別する
iat
Issued AT の略。JWt の発行時間を epoch time で表す
exp
EXPiration time の略。ID トークンの有効期限を epoch time で表す
nonce
リプレイアタック防止のために用いられるランダムな文字列。認証リクエストに含まれる nonce と同じ値が ID トークンに含まれる
署名
JWT の署名部は「ヘッダー + . + ペイロード」に対して Base64 URL エンコードしたものに署名した結果を Base64 URL エンコードした文字列となる。
なぜ署名が必要なのか
ペイロードには sub / iss / aud などのエンドユーザーを識別したり ID トークンの正当性を確認するための重要な情報が記載されている。したがって受け取ったこれらの情報が改ざんされていないことを保証する必要がありそのために発行元である IdP により署名が行われる。
なお、認可コードフローのトークンレスポンスで ID トークンを取得した場合は IdP から直接 RP に ID トークンが渡るため ID トークンの改ざんの可能性は限りなく低くなる。その場合、署名の検証は必須ではない
署名の方式
公開鍵方式のため署名検証のためには公開鍵が必要となる。公開鍵の公開は JWK の仕様は RFC 7517 で規定されている。Google では複数の公開鍵を公開したうえで定期的に変更している。Google / Yahoo Japan / LINE はすべて公開鍵方式の RS256 となっている。 code:keys.json
{
"keys": [
{
"n": "tLZpmdBD-qb8fwqg-DKX8ljpCAAv5n9s5N-JBzOIu3Ry1au3diX_AXKcnpqWJt3Mh3lT4x-zKl4SLpcjpSHYdim4tmqKucUupLTXS-yIqGBw2xDaI0GpYd8QFiFAxTAcwrEoCdl3BGGojo4zmARcHBe_IfeQls097Um3Xu2uiD0RehagoXnDhzk54WAvN05GXJ1xzzx6B7H_fclXcUYb5p5n7SgPDUchTDsDFGCI60Sqqz10d_GNcceThotlXRXcGVlTQ9AGJ_ejzkLWE7NiJc7ZWkrufsNKvVsWT12y66u0VWeopuQZxqSoHIRvSZ71JsBT3dAN897ViZtyYdWoqQ",
"kid": "8e0acf891e090091ef1a5e7e64bab280fd1447fa",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"e": "AQAB"
},
{
"alg": "RS256",
"kid": "a29abc19be27fb4151aa431e94fa3680ae458da5",
"n": "pHEcyF7IosBA_2jKZ0iZt7oLSKcUZFoDdDsyx27xE3tIpYDMpOZATrMePFQKdow0rkhoydTq0YK9RSsW7bh1ORDXb2s4Z6HOVJiDVqtfIfH5ohKSBedaGihYN8RnZIO_XkrlDPztIZxvmsDC5mZnk0wKID4S2gstZlYqx9cblAA9o1rzr_7pf-bs1b9kyX15DNFY_8LJsDBGzRozAukkIIEgfdXG3dBvhyDESh-8qfPeL_I2w8GdY4bHtUTcUmpk1G_UzC74Zv8YGfQVY9ptjw2MTFgnctc0HBjCshioFpt6vSdi-SpyCEr7xx-JLy4YgcsmXBvcBpgh31LqZcylTw",
"use": "sig",
"e": "AQAB",
"kty": "RSA"
}
]
}
それぞれの keys の配列に kid クレームがあり JWT のペイロードに含まれる kid と一致するものを選ぶ。use クレームは利用方法を規定しており、 sig は署名を意味する。kty は署名アルゴリズムを示す。e (exponent) と n (module) が公開鍵の実態。
感じたこと
認可コードフローのトークンレスポンスで ID トークン取得した場合は署名の検証が必要ないというのはなるほどという感じ
UserInfo エンドポイント
UserInfo エンドポイントはエンドユーザーのプロフィール情報を RP に与えるためのエンドポイント。OIDC では ID トークンに含まれる sub クレームでユーザー認証を行う必要があり、OIDC の UserInfo エンドポイントは例えば新規登録のためにエンドユーザーの email や住所を取得したりログイン後に表示するユーザー名を取得するなど、認証以外の目的でユーザー情報を利用する際に用いる。OIDC での UserInfo エンドポイントは仕様として定義されているため OIDC に準拠する IdP では共通となる。UserInfo エンドポイントへのりくえすとは HTTP GET もしくは POST のいずれかで行い、Authorization ヘッダーには Bearer トークンとしてアクセストークンをセットする。
レスポンスには必ず sub クレームが含まれ、もしアクセストークンが入れ替えられていた場合は ID トークンの sub クレームと UserInfo レスポンスの sub クレームの値が異なるため入れ替えを検知できる。そのため RP はこの2つの sub クレームが一致しているかを必ず確認する必要がある。
フロー
RP の事前登録
RP の開発者はリソースを提供する組織に対して RP の情報を登録し、client_id / client_secret の発行を受ける必要がある。開発者が登録する情報で最も大切なものは redirect_uri となる。
ID トークンの検証
ID トークンを受け取った RP は攻撃を防ぐためにペイロードの値と署名を確認する必要がある。ただし、認可コードフローの場合、ブラウザを介さず直接 IdP から ID トークンを受け取るため署名検証をスキップしても問題ない。
sub をエンドユーザーの識別子として利用する
ID トークンには必ず sub クレームが含まれている。RP はこの sub の値をエンドユーザーの識別子として利用しなければならない。ID トークンには email が含まれることもあるが email は変更される可能性もあるため email を使用するべきではない。
iss が利用する IdP の URL であることを確認
iss は IdP の識別子で、 IdP の URL が記載されている。RP は iss の値が利用する IdP のものであることを確認する必要がある。
aud に自分の client_id が入っていることを確認
aud クレームには ID トークンの発行を受ける RP の client_id が入る。したがって、このクレームに自分の client_id が入っていることを確認する。
仮に受け取った ID トークンが他の RP 向けに発行された ID トークンに入れ替えられていた場合 aud クレームを確認することでそれに気づくことができる。aud クレームに自分の client_id が入っていない場合はこの ID トークンで認証を行ってはいけない。
exp が過ぎていないことを確認する
iat が許容できる範囲内であることを確認する
現在時刻から遥かに離れた時間であった場合は拒絶する
nonce のチェック
nonce は悪意のある認可コードの入れ替えを行えないようにするために存在している
署名の検証
IdP が提供する公開鍵を利用して署名を検証する。仮に ID トークンの内容が改ざんされていた場合、署名を検証することで改ざんに気づくことができる。
UserInfo エンドポイントのレスポンスには sub クレームが含まれており、この値が一致しているかどうかを確認する必要がある。
感じたこと
認可コード周りのフローについては自社で開発している OIDC サーバーとネイティブアプリの間の OIDC 認証であるならば必要ない気がしている。すっ飛ばしても大丈夫そう?ただそれだと OIDC と呼べないのか?脆弱性を生んでしまう可能性があるのであればおとなしく認可コード周りのフローもやっておいたほうが無難そうな気がしている。
認可コードはブラウザを介しても良いが、アクセストークンはブラウザをなるべく介さない実装にすべきと捉えた