OAuthとOIDCを認証の観点でまとめる
Webアプリやスマホアプリを作っていると、ログイン機能が欲しくなるのはよくあることだと思います。FWの機能を使うことによって、IDとパスワードを使用したログイン機能をシンプルに実装することはできるかもしれませんが、機密情報であるパスワードはできるだけ保存したくありません。そこで、OAuthやOIDC(OpenID Connect)を使ったソーシャルログインがあります。
この投稿は、ソーシャルログインを実装する際に使用できる技術であるOAuthとOIDCについて調べたことを、自分の理解のためにまとめたものです。
認証と認可
OAuthとOIDCは、それぞれ認証(authentication)と認可(authorization)のための仕様です。認証と認可には簡単に言うと、以下のような違いがあります。
- 認証は「誰であるかを確認すること」
- 認可は「誰かが誰かに対して何らかの権限を与えること」
認証は本人確認であり、登録されているユーザーと同じユーザーなのかを確認することだと言えます。認証方法としてはIDとパスワード・生体認証などがあり、ユーザーを特定できる一意な識別子を使用します。
認可は認証よりも要素が多く、「誰が」「誰に」「なんの権限を」という3つの要素が出てきます。例えば、「ユーザーがアプリにGoogleプロフィールアクセスの権限を与える」といった操作が考えられます。このうち「誰が」という要素を決めるために認証が行われます。
また、認証とセッション管理を混同しないように注意が必要です。セッション管理は認証されたユーザーのログイン状態を管理することを指すので、認証したあとにセッションを開始するという意識を持つ必要があります。混同してしまうと、「認証された」という情報だけでセッション管理を行ってしまい、セッションの有効期限をコントロールできなくなるといった問題が起きる可能性があります。
OAuth/OIDCの概要
OAuthは認可の仕組みであり、OIDCはOAuthに認証の仕組みを追加した拡張仕様だと言えます。そのため、OIDCの処理フローはOAuthがベースになっており、OAuthの処理フローの中で認証のための情報を発行できるようになっています。OAuthは認証方法について範囲外だと明記しているのですが、認証に使われることもあります。これについては後述します。
OAuthは主に、認可と、その結果得られるアクセストークンに関する仕様です。アクセストークンは権限を持っている証であり、このトークンを使用してAPIアクセスを行うことができます。OAuthではアクセストークンの取得方法や利用方法などが定義されています。
OIDCは主に、認証と、その結果得られるIDトークンに関する仕様です。IDトークンは認証されたユーザーの識別子や発行された時刻などの情報が含まれており、認証イベントの情報だと言えます。IDトークンを見ることによって、ある認証に関連する様々な情報を得ることができます。OIDCではIDトークンの構造や取得方法などが定義されています。
トークン
OAuthやOIDCは主にトークンの取得方法に関する仕様です。ここでは、それぞれのトークンがどのような構造を持っていて、どのように使われるかについて見ていきます。
アクセストークン
アクセストークンには好きな形式でデータをもたせることができます。アクセストークンの形式は仕様では定義されていませんが、実際の実装としては識別子型・内包型・ハイブリッド型に分類できます。
- 識別子型: 識別子をアクセストークンに持たせ、別の場所で識別子と紐づく情報を管理する形式
- 内包型: アクセストークンに紐づく情報をアクセストークン自体に埋め込む形式
- ハイブリッド型: 内包型のなかに識別子が入った形式
アクセストークンはAPIアクセスで使われることを目的としており、仕様にはクライアントから見て不透明(opaque)であると書かれています。アクセストークンを解釈するのはあくまでAPI側で、それを使用する側でアクセストークンの中身を確認する使い方は想定されていません。
IDトークン
IDトークンは文字列で表現されます。その構造は以下のようになっています。
<ヘッダー>.<ペイロード>.<署名>
この構造はJWS(JSON Web Signature)という形式です。3つの部分をそれぞれbase64urlでエンコードしたものをピリオドでつなげています。ヘッダーはJSON形式で、検証に使用するアルゴリズムがalg
パラメータに入っています。ペイロードの形式は自由で、JSONや他の形式のデータを含めることができます。署名はalg
パラメータに応じた値が入っています。署名の対象となっているのは<ヘッダー>.<ペイロード>
の値です。alg
パラメータがRS256
であれば、RSAの公開鍵を使用して署名を検証します。
IDトークンでは、JWSのペイロードにJSON形式のデータが入っています。このようにJWSのペイロードにJSON形式のデータが埋め込まれている構造をJWT(JSON Web Token)といいます。JWTでは、JSONのキーバリューのペアをクレーム(claim)と呼びます。
ここまでがIDトークンの形式になります。IDトークンは、JWT形式のデータであり、JWTはJWSのペイロード部分にJSON形式のデータが埋め込まれたものです。
IDトークンでは、認証に関する以下のようなクレームが定義されています。
- iss: トークンを発行したサーバーの識別子
- sub: 認証されたユーザーの識別子
- aud: トークンの発行を依頼したサービスの識別子
- exp: トークンの有効期限
- iat: トークンが発行された日時
OIDCにおけるIDトークンは認証イベントやユーザーを表現するための使われるもので、セッショントークンについての言及はありません。基本的にはIDトークンを使ってユーザーを認証したあと、独自にセッション管理を実装する必要があります。IDトークンでセッション管理を行おうとする場合はOIDCから外れることになるのですが、FirebaseやAuth0のようなIDaaSではセッショントークンとしてIDトークンが使われているので注意が必要です。
登場人物
OAuth/OIDCではロールとロール間のやり取りが定義されています。OIDC/OAuthで似たような役割で別の用語を使うことがありますが、ここでは認証に焦点を当てるために、OIDCでの役割を説明しています。
- <OIDCの用語> / <OAuthの用語>
-
エンドユーザー / リソースオーナー
- 認証しようとしているユーザー
-
Relying Party(RP) / クライアント
- エンドユーザーの許可を得てIdPからトークンを取得するサービス
- ClientSecretを安全に管理できるConfidential・安全に管理できないPublic
-
OpenID Provider(OP)・IdP / 認可サーバー
- トークンを発行するサーバー
- 認可エンドポイント・トークンエンドポイントを持つ
-
UserInfo API
- トークンを使ってユーザーのプロフィール情報を取得するためのAPI
- OIDCでは仕様で定義されているが、OAuthでは定義されていない
- OAuthではリソースを管理しているリソースサーバーで実装される事が多い
WebアプリにGoogleアカウントでログインできる場合、エンドユーザーはログインしたいユーザー、クライアントはWebアプリ、IdP・UserInfo APIはGoogleになります。
クライアントはClientSecretを安全に管理できるかによってConfidential・Publicクライアントに分かれています。例えばWebアプリのバックエンドはConfidentialであるとみなせますが、SPAやモバイルアプリ、デスクトップアプリはPublicです。Publicなクライアントは、バイナリ解析でClientSecretが取得できることがあります。また、攻撃者が自身のクライアントを改ざんすることも可能なため、検証の意味がなくなるケースもあります。
IdPには認可エンドポイントとトークンエンドポイントがあり、後述するフローによって何が使用されるかが決まっています。トークンエンドポイントは名前のとおりトークンを返すエンドポイントで、ClientSecretなどでクライアントの認証を行う必要があります。認可エンドポイントはトークンエンドポイントに必要な情報を取得するためのエンドポイントですが、認可エンドポイントからトークンが返ってくる特殊なフローもあります。
UserInfo APIはOIDCでは定義されているのですが、OAuthでは仕様の中に存在しません。OAuthではリソースが保管されているリソースサーバーに独自仕様のUserInfo APIが存在することになります。OAuthによる認証のためにはUserInfo APIが必須なため、厳密に言うとOAuthだけで認証することはできません。OIDCではUserInfo APIがなくても認証することができます。
認証フローの概要
OAuthのフロー
OAuthでは認可を行うための4つのフローと、アクセストークンの再発行のためのフローが定義されています。認可のフローではありますが、UserInfo APIがある場合は認証を行えます。
-
認可コードフロー
- 基本的なフロー
- 認可エンドポイントから認可コードを取得し、認可コードを使用してトークンエンドポイントからトークンを取得する
-
インプリシットフロー
- 利用を避けるべきフロー
- 認可エンドポイントから直接トークンを取得する
-
リソースオーナー・パスワード・クレデンシャルズフロー
- 使ってはいけないフロー
- クライアントにID/PWを渡し、トークンエンドポイントからトークンを取得する
-
クライアント・クレデンシャルズフロー
- ユーザーが関与しないフロー
- クライアントのIDとSecretを使ってトークンエンドポイントからトークンを取得する
-
リフレッシュトークンフロー
- トークンを再発行するフロー
- リフレッシュトークンを使ってトークンエンドポイントからトークンを再発行する
OAuthでは、認可エンドポイントへのリクエストを認可リクエスト、それに対する認可コードやトークンが含まれるレスポンスを認可レスポンスと呼びます。また、トークンエンドポイントへのリクエストをトークンリクエスト、レスポンスをトークンレスポンスと呼びます。
認可リクエストにはresponse_type
パラメータが存在しており、code
の場合には認可コードフロー、token
の場合にはインプリシットフローを要求します。
上記のフローでアクセストークンを取得することができるので、UserInfo APIにアクセスしてプロフィール情報を取得し、ユーザーの識別子を使ってログイン処理を行えます。
OIDCのフロー
OIDCでは認可リクエストのscope
パラメータにopenid
を含めることで、IDトークンの発行を可能にしています。また、同じく認可リクエストのresponse_type
パラメータにid_token
を含めることで、認可エンドポイントでIDトークンを発行することができます。
このような拡張のため、OIDCでは認可リクエストは認証リクエストと呼ばれることもあります。また、あわせてレスポンスも認証レスポンスと呼ばれることもあります。
以下はscope
パラメータにopenid
が含まれていてる場合のOIDCのフローの一部です。
-
response_type=code
- OAuthの認可コードフローがベース
- 認可コードでトークンエンドポイントからアクセストークンと一緒にIDトークンを取得する
-
response_type=code id_token
- OAuthの認可コードフローがベース
- 認可エンドポイントから認可コードと一緒にIDトークンを取得する
-
response_type=id_token
- OAuthのインプリシットフローがベース
- 認可エンドポイントから認可コードではなくIDトークンを取得する
上記のフローでIDトークンを取得することができるので、IDトークンに含まれる識別子(sub)を使用してログイン処理を行えます。
認証フローの詳細
一般的に使用されるOAuth/OIDCの認可コードフロー + Confidentialクライアントを例にとって、ログインのフローを具体的に解説していきます。フローは以下のようになります。
-
ログインリクエスト
- エンドユーザーがクライアントにログインのためのリクエストを送ります
- 「Googleアカウントでログイン」のようなボタンを押すことになると思います
-
IdPにリダイレクト・認証リクエスト
- クライアントはログインリクエストを受け取ると、IdPへの認証リクエストのためのURLを生成し、そのURLへのリダイレクトレスポンスとして返します
- OIDCを使用するので
scope
にopenid
を含め、認可コードフローなのでresponse_type
にcode
を渡すURLを作成します- URL例:
/authorize?response_type=code&scope=openid...
- URL例:
-
認証画面・認証
- 認証リクエストのレスポンスとして、ユーザーに認証画面が返されるので認証を行います
- このやり取りは必ず実行されるわけではなく、IdPの実装によっては、すでに認証している場合には省略されることがあります
- WebアプリにGitHubアカウントでログインできる場合で、すでにGitHubのサイトでログインしているケースなど
- OIDCでは認証リクエストの
prompt
パラメータにnone
を指定することで常に省略でき、認証していない場合には次の処理でエラーレスポンスが返ってきます
-
クライアントにリダイレクト・認証レスポンス
- エンドユーザーの認証が終わると、IdPは認可コードを発行して事前に登録されているクライアントのURLに埋め込み、そのURLへのリダイレクトレスポンスとして返します
- URL例:
/login/callback?code=...
- URL例:
- エンドユーザーの認証が終わると、IdPは認可コードを発行して事前に登録されているクライアントのURLに埋め込み、そのURLへのリダイレクトレスポンスとして返します
-
トークンリクエスト・レスポンス
- クライアントはIdPからのコールバックによって認証レスポンスから認可コードを取得できた場合、その認可コードを使ってトークンエンドポイントからトークンを取得します
- この例ではアクセストークンとIDトークンが取得できます
-
プロフィール情報のリクエスト・レスポンス (OAuthのみ)
- OAuthで認証を実装する場合には、ここでUserInfo APIにアクセスしてユーザー識別子を含むプロフィール情報を取得します
- 前述した通り、OAuthの仕様にはUserInfo APIは存在しないため、独自実装されたUserInfo APIを使うことになります
-
識別子で認証
- OAuthではプロフィール情報のユーザー識別子、OIDCではIDトークンの識別子(sub)を使用してユーザーを認証します
- ここでは例えば、クライアントが管理しているユーザーの情報とユーザー識別子を比較して、存在すればログイン状態にするなどの処理が考えられます
- 認証の後にセッションを開始させるような処理もあると思います
-
ログインレスポンス
- ログインの合否のレスポンスなどを返します
- ログインに成功した場合は、cookieにセッションIDがセットされているかもしれません
OAuthを使用した認証について
OAuthは認可のための仕組みなのですが、上の図ではOAuthを使用した認証も行えることを表しています。(UserInfo APIはOAuthの仕様にはないので、OAuthの使用だけで認証が実装できるわけではないのですが・・・)上のように、クライアントがConfidentialであり、エンドユーザーのアクセストークンであること・アクセストークンから取得したユーザー識別子がエンドユーザーのものであることが保証できる場合、認証を安全に実装することはできます。
一方で、クライアントがPublicな場合には、「エンドユーザーのアクセストークンであること」を保証するのが難しいことがあります。例えば、モバイルアプリ(Publicなクライアント)とWebアプリ(Confidentialなクライアント)があり、モバイルアプリからWebアプリにアクセストークンを渡して、WebアプリがUserInfo APIにアクセスして認証するような場合です。エンドユーザーのアクセストークンであることを保証するのが難しいため、アクセストークンが漏れると勝手に使用される可能性があります。この場合、認可サーバーがアクセストークンとクライアントのセッションを紐づけておく必要があり、認可サーバーにも仕様から外れた独自実装が必要になります。
また、OAuthは認証のための仕様ではないため、認証の複雑な要件に対応するための労力が大きいです。認証の必要最低限の要件は、ログインしようとしているユーザーの識別子を取得できることです。これはOAuth + UserInfo APIで比較的簡単に実現できるのですが、更に複雑な要件を実現するためには、すべて自分で設計して実装する必要があります。
複雑な要件の例としては、認証方法の指定や、クライアントとIdP間のセッション管理などがあります。OIDCには、認証方法の指定は認証コンテキストクラスというものを認証リクエスト時に指定できるような仕様があったり、セッション管理についての仕様が存在しています。
認証では、OIDCが使えるのならば使わない理由はないです。OIDCはOAuthと違って仕様の範囲内で認証を実装することができ、複雑な要件も仕様でサポートされていることがあります。
セキュリティ対策
認証フローの中でセキュリティ対策については無視していましたが、いくつかの攻撃が考えられます。ここではその対策として使用できる技術について紹介していきます。
OAuth/OIDCへの攻撃としてCSRFがありますが、一般的なCSRFとは目的が少し異なっていることが特徴です。一般的なCSRFでは、攻撃者が標的となったユーザーになりすまして何らかの処理を実行する事が多いのですが、OAuth/OIDCでは、攻撃者が標的となったユーザーに自身のリソースを紐づけることを目的としています。
攻撃者は標的ユーザーに自身のリソースを紐づけることで、標的ユーザーのプライベートなデータにアクセスできたり、攻撃者のアカウントで標的ユーザーとしてログインできてしまいます。前者は、クライアントが外部のGoogleストレージにデータを保存するようなケースで、攻撃者のGoogleストレージに紐づけられると、攻撃者のGoogleストレージに標的ユーザーのデータが保存されることになります。後者は、クライアントがGoogleログインを実装しているケースで、攻撃者のGoogleアカウントと連携すると、攻撃者のGoogleアカウントで標的ユーザーとしてログインすることができてしまいます。
これから紹介するセキュリティ対策は、値が同一のセッションに紐づいていることを検証するのですが、セッションをただの「一連の処理」という意味で使っています。具体的には、ユーザーとクライアントとIdP間の、認証処理の開始から最終的なレスポンスを受け取るまでの一連の処理をセッションと呼んでいます。一般的にセッションと言うとログインセッションことを指すことが多いと思うのですが、ここではそのような意味では使っていません。
セッションと値を紐づける方法としては、cookieに値をセットするのが手軽な方法だと思います。Webアプリではcookieを使うことによって特定のユーザー(ブラウザ)とクライアント(Webアプリ)で閉じたデータのやり取りができるようになります。ログインセッションのように、cookieにはIDだけを含めて、バックエンドで値を管理することもできるとは思います。
state
stateパラメータは、認証リクエストと認証レスポンスが同一のセッションに紐づいていることを確認するために使用できるパラメータで、CSRF攻撃の対策として使われます。認証リクエスト時にセッションに紐づくstateを生成して送信すると、渡したstateが認証レスポンスに含まれているため、セッションに紐づくstateと比較して検証できます。
想定される認可コードフローにおけるCSRF攻撃は以下のような流れになります。
攻撃者は正規のクライアントでフローを開始して、クライアントへの認証レスポンスのリダイレクトをキャンセルして、そのURLを標的ユーザーに送ります。標的ユーザーは、自分が送信していない認証リクエストへの認証レスポンスをクライアントに送ってしまい、攻撃者のリソースと紐づけられてしまいます。
この攻撃が発生してしまうのは、認証リクエストを発行したセッションと認証レスポンスを受け取ったセッションが異なっていても、通常通り処理が進んでしまうからです。クライアントとIdPは同じなのですが、ユーザーが攻撃者から標的ユーザーに変わっているため、セッションが異なっているとみなしています。
この攻撃への対策の流れは以下のようになります。
- クライアントはログインリクエストを受け取ると、stateを生成してセッションと紐づけ、認証リクエストのstateパラメータに付与する
- セッションに紐づける方法としては、リダイレクトの認証リクエストで
set-cookie
にstateを含める方法などがあります
- セッションに紐づける方法としては、リダイレクトの認証リクエストで
- IdPは認証リクエストで受け取ったstateパラメータを認証レスポンスに含める
- クライアントは認証レスポンスを受け取ったら、セッションに紐づくstateと認証レスポンスに含まれているstateを比較して検証する
- 上のように
set-cookie
でstateをセットすると、認証レスポンスのcookie
としてセッションに紐づくstateを取得できます
- 上のように
- 攻撃者と標的ユーザーのセッションは別であり、攻撃者が標的ユーザーのstateを事前に知ることができず、検証に失敗する
注意点は、stateの生成・検証を行うのはクライアントであるということです。仕様としてstateパラメータは定義されていますが、クライアントはそれを正しく実装しない可能性があります。例えばstateをセッションに紐づく値ではなく、完全に固定している場合には意味がありませんし、そもそもクライアント側で検証しないといった実装も考えられます。
また、認証レスポンス自体はセッションに紐づいていることを確認できるのですが、認証レスポンスに含まれる認可コードの値も必ず紐づいているとは言えません。認証レスポンスはブラウザを通したリダイレクトになっており、認可コードの書き換えを検出できない可能性があります。
このような状況は、response_type=code id_token
を認証リクエストに含めて、認可コードと一緒にIDトークンを取得することで対処できます。response_type
が上のように指定されると、IDトークンにc_hash
と呼ばれる認可コードのハッシュ値が含まれるので、これを検証することで改ざんを検出できます。
nonce
nonceパラメータは、OIDCにおいて認証リクエストと特定のレスポンスが同一のセッションに紐づいていることを確認するために使用できるパラメータで、IDトークンのリプレイアタックの対策として使われます。どのレスポンスと紐づいているかはフローによって異なります。認証リクエスト時にセッションに紐づくnonceを生成して送信すると、渡したnonceがIDトークンのnonceクレームに含まれているため、セッションに紐づくnonceと比較して検証できます。
OIDCではIDトークンの取得方法としてresponse_type
にid_token
を含めて認可コードと一緒に取得する方法と、トークンエンドポイントから取得する方法で2種類あり、それぞれでセッションがどのレスポンスに紐づいているかが変わってきます。
response_type
にid_token
を含めて認可コードと一緒にIDトークンを取得する場合、認証リクエストと認証レスポンス、トークンリクエスト・レスポンスが同一のセッションに紐づいていることを確認できます。このケースは、認証リクエストと認証レスポンスが同一セッションに紐づいていることを検証できるため、stateパラメータの代わりに使うことができます。
IDトークンをトークンエンドポイントから取得する場合、認証リクエストとトークンレスポンスが同一のセッションに紐づいていることを確認できます。上の方法と比べるとIDトークンを取得するタイミングが遅いため、IDトークンを取得する前の、認証レスポンスとトークンリクエストの時点ではそれらがセッションに紐づいていることを確認できません。トークンレスポンスが紐づいていることが確認できた段階で、それ以前のやりとりも紐づいていることが保証されます。
また、nonceはリプレイアタックを防ぐことができるのですが、この攻撃はフローによって難易度が変わってきます。Confidentialクライアントの認可コードフローの場合はクライアントとIdP(トークンエンドポイント)のやり取りに介入する必要があるので難易度が高いです。一方でインプリシットフローの場合は、IDトークンが流出する可能性が高かったり、IDトークンを書き換えるのが難しくないため、難易度が低いです。
以下の図はConfidentialクライアント + インプリシットフローに対してのIDトークンのリプレイアタックを想定しています。そういった事例があるかわからないのですが、クライアント側でClientSecretを保管したくない場合に使われることがあるかもしれません。OIDCではトークンエンドポイントへのリクエストでクライアント認証が行われますが、認可エンドポイントでは行われないため、Secretを保管する必要がなくなります。
攻撃者はどうにかして事前に標的ユーザーのIDトークンを取得しておき、認証レスポンスをキャンセルしてIDトークンを書き換えます。クライアントは受け取ったIDトークンの識別子を確認して、標的ユーザーとして認証してしまいます。
この攻撃が発生してしまうのは、トークンレスポンスを受け取ったセッションと認証リクエストを発行したセッションが異なっていても通常通り処理が進んでしまうからです。トークンレスポンスを受け取った標的ユーザーのセッションはすでに終了しており、攻撃者が新しいセッションを開始しているため、セッションが異なっているとみなしています。
この攻撃への対策の流れは以下のようになります。
- クライアントはログインリクエストを受け取ると、nonceを生成してセッションと紐づけ、認証リクエストのnonceパラメータに付与する
- セッションに紐づける方法として、HttpOnly属性付きのcookieに保存する方法があります
- 攻撃者は標的ユーザーのIDトークンのnonceを知っているので、cookieが書き換えられないようにHttpOnly属性が必要です
- セッションに紐づける方法として、HttpOnly属性付きのcookieに保存する方法があります
- IdPは認証リクエストで受け取ったnonceパラメータをIDトークンのnonceクレームに含める
- クライアントはIDトークンを受け取ると、セッションに紐づくnonceとIDトークンのnonceクレームを比較して検証する
- 攻撃者は標的ユーザーのIDトークンのnonceを知ることはできるが、攻撃者と標的ユーザーのセッションは別であり、攻撃者自身のセッションに紐づくnonceを書き換えることができず、検証に失敗する
この方法もstateパラメータと同じく、クライアントが値を生成・検証を行うことに注意する必要があります。常に同じ値が生成されたり、検証していない場合には意味がありません。
PKCE
PKCE(Proof Key for Code Exchange by OAuth Public Clients)は、認証リクエストとトークンリクエストが同一のセッションに紐づいていることを確認するために使用できる技術で、認可コード横取り攻撃の対策やCSRF攻撃の対策として使われます。認証リクエスト時にセッションに紐づく値を生成し、それをハッシュ化した値を送信した後、トークンリクエストにセッションに紐づく値を渡すことで、IdP側でハッシュ化して検証できます。
以下の図は、モバイルアプリ(Publicクライアント)での認可コード横取り攻撃を想定しています。モバイルアプリではカスタムURIスキームによってクライアントへのリダイレクトが実装されることがあり、正規のクライアントと同一のRedirect Endpointによって起動するアプリを作成されると、以下のように認証レスポンス内の認可コードが奪われる可能性があります。
攻撃者が作成したモバイルアプリに認可コードを横取りされてしまうため、正規のクライアントになりすまして、標的ユーザーのトークンを取得できてしまいます。
この攻撃が発生してしまうのは、認証リクエストを発行したセッションとトークンリクエストを発行したセッションが異なっていても通常通り処理が進んでしまうからです。ユーザーとIdPは同じですが、クライアントが異なっているため、セッションが異なっているとみなしています。
この攻撃への対策の流れは以下のようになります。
- クライアントはログインリクエストを受け取ると、code_verifierを生成してセッションと紐づけ、code_verifierのハッシュを取って認証リクエストのcode_challengeパラメータに付与する
- セッションと紐づけるために、code_verifierをクライアントに保存することができます
- 実際にはcode_challenge_methodパラメータでハッシュのアルゴリズムも指定しています
- クライアントがトークンリクエストの際にセッションに紐づくcode_verifierをIdPに送り、IdPがハッシュを取って認証リクエスト時のcode_challengeと比較して検証する
- 攻撃者のクライアントは正規のクライアントとは別のセッションになるため、認証リクエストを発行した時点でのcode_verifierを知ることができず、検証に失敗する
PKCEはstateやnonceと同じように値の生成はクライアントで行うのですが、検証はIdPが行うことになります。code_verifierとして同じ値を生成していると意味がなくなるのですが、クライアントではなくIdPで検証することが定義されているため、検証忘れは防げます。
認可コードフローを使用する場合には、PKCEはCSRF対策として使用できるためConfidentialクライアントでも推奨されています。stateで紹介したCSRF攻撃の対策は以下のようなものでした。
stateによってCSRF対策を行っていますが、PKCEでも対策することができます。stateでは、クライアントがトークンリクエストを送信する前に、認証レスポンスのstateを検証することによってCSRF対策を行っていますが、stateではなくPKCEを使用しても、攻撃者と標的ユーザーのセッションは異なるため、トークンリクエストに適切なcode_verifierが送られることはなく、CSRF対策として使用できます。
PKCEだけでCSRF対策を行うことは可能なのですが、IdPへの負荷を考えるのであれば、stateによって早い段階でチェックする方が良いと思います。stateを使用せずにPKCEだけをCSRF対策として使用すると、本来事前に弾けるはずのトークンリクエストをIdPに送ることになります。stateで事前に認証レスポンスを検証することで、IdPへの負荷を減らすことができます。
さいごに
ソーシャルログインの実装に使用できるOAuthとOIDCについてまとめました。
アプリに認証機能を実装する際には考えなければいけないことが多く、独自実装では脆弱性を埋め込んでしまう可能性が高くなってしまいます。認証機能はID管理機能の一つでしかなく、IDのライフサイクルに応じた様々な機能を実装する必要も出てきます。そのため、まずはIDaaSの使用を検討するのが良いと思います。
参考資料
この投稿は、以下の資料を独自に解釈してまとめたものです。
- The OAuth 2.0 Authorization Framework
- OpenID Connect Core 1.0 incorporating errata set 1
- OAuth 2.0 Security Best Current Practice
- OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る
- OAuth & OIDC 勉強会 【入門編】
- OAuth認証とは何か?なぜダメなのか - 2020冬
- OAuth & OpenID Connect 関連仕様まとめ
- OAuth 2.0 全フローの図解と動画
- OpenID Connect 全フロー解説
- OAuth アクセストークンの実装に関する考察
- IDトークンが分かれば OpenID Connect が分かる
- SPA+Backend構成なWebアプリへのOIDC適用パターン
- そのIDTokenの正体はセッショントークン?それともアサーション?
- 図解:OAuth 2.0に潜む「5つの脆弱性」と解決法
- OAuth 2.0/OpenID Connectで使われるBindingの仕組みについて整理する
- OIDCのImplicit FlowでClientSecretを使わずにID連携する
- OAuth 2.0 / OpenID Connectにおけるstate, nonce, PKCEの限界を意識する
- Identity Lifecycleを意識したID管理機能の設計
- え? OAuth 2.0 の Access Token も JWT じゃなかったの?と思っている皆さん
Discussion