(※この記事は 別媒体に投稿した記事 のバックアップです。 canonical も設定しています)
2021-01-30
※この記事は別アカウント(hyiromori)から引っ越しました
最近、個人的に認証認可周りを学習していて、今回は OpenID Connect について学習したのでその内容をまとめた記事です。
世の中には既に OpenID Connect に関する優れた書籍やブログ記事が沢山ありますが、自分が学習する過程で色々なものを読むことでより理解が深まったと思うので、自分も学習したものをアウトプットすることで同じように学習している人の理解の助けになればと思い書きました。
まだ私も学習中なので、もし間違ったところなどあればコメント頂けるとありがたいです。
OAuth2.0をベースにして(認可だけでなく)認証も行えるようにした拡張仕様です。
なぜOAuth2.0が認証に使えないかというと、以下のように認証に使ってしまうとリスクが非常に高いからです。
https://www.sakimura.org/2012/02/1487/
ざっくりかいつまんで言うと、以下のようになります。
こういった事情から、OAuth2.0で発行されたアクセストークンを認証に使ってはいけません。
:::message
ちなみに認証に使えないことはOAuth2.0の問題ではありません。
OAuth2.0はあくまで「認可」を対象にした仕様なので、対象外の用途である「認証」に使って問題が発生することは当然とも言えます。
:::
一番大きな違いは「IDトークン」が発行されることです。
IDトークンはアクセストークンとは異なり「いつ」「どこで」「なんのために」発行されたトークンなのかの情報を含んでおり、かつ署名されているため改ざんができない(改ざんを検知できる)ようになっています。
OpenID Connect と OAuth2.0 はIDトークン以外にも違いはあります。
(例:OAuth2.0のクライアントをリライング・パーティーと呼ぶなど用語が違ったり、UserInfoエンドポイントを実装するなど)
ただ私が学習していて、網羅的な説明だと初学者には一番重要なところがどこか分かりづらい、ということを私は経験しました。
なので、この記事ではIDトークンとOAuth2.0のフローの差分についてのみ書こうと思います。
なおOAuth2.0の流れをまとめてみる の続きという位置づけで書こうと思いますので、OAuth2.0がよく分からないという方はこちらを先に読むことをおすすめします。
IDトークンは認証したことを証明するためのトークンという位置づけです。
具体的には JWT (JSON Web Token) という形式の文字列になっています。
この JWT に認証に関する情報が含まれています。
JWT (JSON Web Token) とは、任意のJSONデータを格納できるURLセーフな文字列です。
構成としては以下のような3つの情報を .
(ピリオド) でつなげた文字列になっています。
(ヘッダー).(クレーム).(署名)
トークンの形式と署名のバージョンを、URLセーフなBase64でエンコードされたJSON文字列です。
Base64でエンコードされた、任意のデータを含むJSON文字列です。
(※なおクレームには日本で使われる「苦情」のような意味合いは含まれないのでご注意ください)
RFCには予約済みのフィールドもありますが、必須ではありません。
また任意のフィールドを追加することも可能です。
OpenID Connect ではここに認証に関する情報を設定します。
ヘッダーとペイロードに対しての署名の文字列です。
これによって正しい発行者が発行したIDトークンであることを確認できます。
私が実際にGoogleから取得したIDトークンを例に見てみましょう。
(※文字数が多い値や秘匿したい値は ...
としています)
$ jwt decode 'eyJh...8UOg'
Token header
------------
{
"typ": "JWT",
"alg": "RS256",
"kid": "783ec031c59e11f257d0ec15714ef607ce6a2a6f"
}
Token claims
------------
{
"at_hash": "u1...XA",
"aud": "95...8u.apps.googleusercontent.com",
"azp": "95...8u.apps.googleusercontent.com",
"email": "hyiromori@gmail.com",
"email_verified": true,
"exp": 1611135338,
"iat": 1611131738,
"iss": "https://accounts.google.com",
"sub": "10...08"
}
(jwt
コマンドはこちらを使用しました)
https://github.com/mike-engel/jwt-cli
Token claims
の部分が実際の認証に係るデータです。
特に注目すべきなのは iss (ISSuer)
, aud (AUDience)
, exp (EXPiration)
です。
iss (ISSuer)
はトークンの発行者、aud (AUDience)
はトークンの受け手を指しています。aud
は設定時にGoogleから発行された値で、これを検証することでGoogleが自サイト向けの認証時に発行されたことが確認できます。
exp (EXPiration)
はトークンの有効期限(UNIX時間)です。
これを検証することで、有効な(期限切れでない)トークンであることを確認できます。
なお iat (Isused AT)
はIDトークンが発行された日時(UNIX時間)なので、これと有効期限を比較するとGoogleのIDトークンの生存時間が分かります。
実際に計算してみると 3,600秒なので1時間ほどで切れるようになっています。
1611135338 - 1611131738 = 3600 (sec)
このようにきちんと検証することで「正しい発行者が」「自サイト向けに」「1時間以内に認証して発行された(Googleの場合)」IDトークン以外は受け付けないようにできます。
これで OAuth2.0 のアクセストークンをそのまま使った場合の起きるなりすましのような脆弱性が解消されました。
この記事内ではOAuth2.0で定義されているフローの1つ、認可コードによる付与(Authorization Code Grant)をベースに差分を説明します。
OAuth2.0の流れをまとめてみる との差分だけ記述しますので、OAuth2.0がまだあまり理解できていない方はこちらも合わせてご覧ください。
また差分をわかりやすくするため、図や用語などは OAuth2.0 のままとなっているのでご注意ください。
Googleと連携したい時に、クライアントから必要なパラメーターを付与して、Googleの認可サーバーへのリダイレクトさせます。
その際に scope
に openid
を指定する、というのが OAuth2.0 と OpenID Connect の違いです。
クライアント(Webアプリ)内のリンクなどをクリックすると、リダイレクト専用のエンドポイント(例: /request_authorize
)にアクセスします。
GET /request_authorize HTTP/1.1
Host: https://client.example.com
このエンドポイントからは必要な情報をクエリパラメーターにセットされたURLが返却されて、Googleの認可サーバーへリダイレクトします。
HTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=(CLIENT_ID)&redirect_uri=https%3A%2F%2client.example.com%2Fcallback&scope=openid%20email&response_type=code
:::messagescope=openid
のように openid
というスコープが追加されます。
:::
(ここは特に変更ありません)
(ここも特に変更ありません)
リダイレクト時に付与された認可コードを認可サーバーに渡して、アクセストークンと IDトークン を取得します。
ここで正しく検証を行うことで、自サイト向けに認証が行われたことを確認できます。
意外と OpenID Connect と OAuth2.0 の差分は多くないですね。
OpenID Connect の重要なポイントはIDトークンが発行されるということでした。
私はIDトークンがなぜ必要で、どうして安全に認証に使えるのかが理解できた時、OpenID Connect について理解できた気がしました。
なるべく OpenID Connect の概要が分かりやすいように、最低限のものに絞って書いてみました。
実際に使うには情報が不足していると思いますが、OpenID Connect の概要を理解する助けになれば幸いです。
学習する過程で参考になった資料をまとめておきます。