OAuth2.0によるソーシャルログインは危険ではない~PKCE,OIDCの話を添えて~

# TL;DR

1. 本記事はプロバイダー側がOIDCを利用できないでOAUTHのみを提供している状態でソーシャルログインを可能な限り安全に実装したい場合に参考になると思います。
1. ただ、最後にまとめていますがOIDCを利用できる時は利用をした方が望ましいです。

:::note alert 
私はセキュリティエキスパートではありません。執筆当時（2024/11/22）にアクセスできる正確な情報を提供するよう努めていますが、参考程度にこちらの記事を利用していただければと思います。

また、本ドキュメントでのグラントタイプは`authorization_code`を前提としています。ネイティブアプリケーションやSPAの場合は、`implicit`や`hybrid`を使うこともありますが、それについては別途考える必要があることに注意してください。
:::

# 1. OAuth2.0による認証は危険。。。！？

「OAuth2.0は認可のプロトコルだから、認証には使ってはだめだよ！」

こんな意見をブログや技術記事で目にすることがあります。

1. 都元ダイスケ, 2018, [OAuth 認証を真面目に考える | DevelopersIO](https://dev.classmethod.jp/articles/deep-think-about-oauth-authentication/)
1. yuto.inoue, 2024, [OAuth 2.0の認証と認可とは？初心者向けにわかりやすく解説](https://techblog.asia-quest.jp/202410/what-is-oauth-2.0-authentication-and-authorization)
1. bwkw, 2024, [OIDC を Hono × Bun で完全に理解する](https://zenn.dev/levtech/articles/what-is-oidc)
1. ritou, 2020, [OAuth認証とは何か?なぜダメなのか - 2020冬 - r-weblife](https://ritou.hatenablog.com/entry/2020/12/01/000000)

ですが、それについて具体的にどういうことなのか、どうすれば安全に認証を実装できるのか、理解している人は少ないのではないでしょうか。

そこで、本記事ではOAuth2.0とOpenID Connect（OIDC）の違いや、OAuth2.0を使った認証の危険性、安全な認証のためのポイントについて解説します。

また、ScalaのPlay Frameworkを使ってDiscordのOAuth2.0を使った認証を実装したため、適宜参考にしていただけると幸いです。
- [makinzm/social-login-with-scala-tidb-discord: ScalaでDiscordのOAuth2.0を使った認証を実装した](https://github.com/makinzm/social-login-with-scala-tidb-discord)
    - 特に次のCommitは本記事で取り上げているPKCEに関する実装のため参考になるかもしれません：[security: PKCE to deal with authZ code interception attack · makinzm/social-login-with-scala-tidb-discord@fc1b11b](https://github.com/makinzm/social-login-with-scala-tidb-discord/commit/fc1b11bdab0d1acf9aad63c216c77ac8792de4c7)

# 2. やりたいこと

説明の都合上、以下のようなシチュエーションを考えてみます。

私たちは、ユーザーがログインして自分のアカウント情報や個人設定を管理できるWebアプリケーションを開発しています。ユーザー体験を向上させるために、ソーシャルログイン機能を導入し、GoogleやFacebookなどの既存のアカウントでログインできるようにしたいと考えています。

また、プロバイダーによってOAUTHだけ提供されOIDCが提供されていない状況もあり、今回はOIDCが提供されていない場合に安全に認証をする方法について考えていきます。

```mermaid
sequenceDiagram
    participant User as ユーザー
    participant App as Webアプリケーション
    participant Provider as ソーシャルログインプロバイダー<br/>(GoogleやFacebook)

    User->>App: ログインページにアクセス
    App->>User: ソーシャルログインボタンを表示
    User->>App: ソーシャルログインボタンをクリック
    App->>Provider: 認可リクエストを送信
    Provider->>User: 認証画面を表示
    User->>Provider: 認証情報を入力
    Provider->>Provider: ユーザーを認証
    Provider->>User: 認可コードを含むレスポンス
    User ->>App:　認可コードを持ちリダイレクト
    App->>Provider: 認可コードでアクセストークンをリクエスト
    Provider->>App: アクセストークンを返却
    App->>Provider: アクセストークンでユーザー情報をリクエスト
    Provider->>App: ユーザー情報を返却
    App->>User: ログイン成功、アカウント情報を表示
```
参考：
1. [OAuth 2.0 Authorization Code Flow with PKCE | Docs | Twitter Developer Platform](https://developer.x.com/en/docs/authentication/oauth-2-0/authorization-code)
1. [アクセストークンを取得する - freee Developers Community](https://developer.freee.co.jp/startguide/getting-access-token)
1. [Microsoft ID プラットフォームと OAuth 2.0 認証コード フロー - Microsoft identity platform | Microsoft Learn](https://learn.microsoft.com/ja-jp/entra/identity-platform/v2-oauth2-auth-code-flow)
1. [Browser Access OAuth 2.0 Flows | Cloud Sundial](https://cloudsundial.com/salesforce-browser-access-oauth-flows)

(図中の認可コードを含むリダイレクトは、実装によってはUserのブラウザに対して行われ、その後にブラウザからAppにリダイレクトされる場合がありますが、今回の例では直接Appにリダイレクトされるものとします)

このとき、OAuth2.0を使用して認証を実装していますが、「OAuth2.0は認可のプロトコルであり、認証には適していない」という意見もあり、少し不安です。

# 3. OAuth2.0とOIDCの違い

OAuth2.0はリソースへのアクセス許可（認可）を扱います。

> OAuth 2.0 は, サードパーティーアプリケーションによるHTTPサービスへの限定的なアクセスを可能にする認可フレームワークである
> [The OAuth 2.0 Authorization Framework](https://openid-foundation-japan.github.io/rfc6749.ja.html) より

OpenID Connect（OIDC）は、OAuth2.0をベースにした認証プロトコルです。

> OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.
> [Final: OpenID Connect Core 1.0 incorporating errata set 2](https://openid.net/specs/openid-connect-core-1_0.html?utm_source=chatgpt.com) より

具体的には、OIDCはOAuth2.0のフローにIDトークン（JWT形式）を追加し、ユーザーのプロフィール情報や認証イベントの詳細を安全にクライアントに伝達します。

> Information about the authentication performed is returned in a JSON Web Token (JWT) [JWT] called an ID Token (see Section 2). OAuth 2.0 Authentication Servers implementing OpenID Connect are also referred to as OpenID Providers (OPs). OAuth 2.0 Clients using OpenID Connect are also referred to as Relying Parties (RPs).
> [Final: OpenID Connect Core 1.0 incorporating errata set 2](https://openid.net/specs/openid-connect-core-1_0.html?utm_source=chatgpt.com) より

今回のシチュエーションにおいて、OIDCを使う場合以下のようになります(★マークがついている部分がOIDCの特徴です)：

```mermaid
sequenceDiagram
    participant User as ユーザー
    participant App as Webアプリケーション
    participant Provider as ソーシャルログインプロバイダー<br/>(GoogleやFacebook)

    User->>App: ログインページにアクセス
    App->>User: ソーシャルログインボタンを表示
    User->>App: ソーシャルログインボタンをクリック
    App->>Provider: 認可リクエストを送信
    Provider->>User: 認証画面を表示
    User->>Provider: 認証情報を入力
    Provider->>Provider: ユーザーを認証
    Provider->>User: 認可コードを含むレスポンス
    User ->>App:　認可コードを持ちリダイレクト
    App->>Provider: ★認可コードでアクセストークンとIDトークンをリクエスト
    Provider->>App: ★アクセストークンとIDトークンを返却
    App->>App: ★IDトークンを検証し、ユーザー情報を取得
    App->>User: ログイン成功、アカウント情報を表示

```
参考
1. [Auth0 by Okta, Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-authorization-code-flow-works)

これにより、クライアントはユーザーが誰であるかを確実に知ることができます。

# 4. OAuth2.0の認証は何が危険なの？

IDトークンを使わない場合、OAuth2.0を使った認証には以下のようなリスクがあります：

- **認可コードの漏洩**: 認可コードが第三者に盗まれると、アクセストークンを取得されるリスクがあります。
- **認可コードの置き換え**: 認可コードを第三者に盗まれ、第三者が自身の認可コードをユーザーに誤って使用させることで、アクセストークンを取得されるリスクがあります。

上記に対して、OIDCを使うと、認可コードの置き換えに対して対策をすることができます（ただし、認可コードの漏洩に対しては別途対策が必要で後述するOAuth2.0の対策と同じです）。

# 5. OIDCで解決できる問題

OIDCを使うことは、IDトークンを使うことと同義です。

IDトークンには`c_hash`や`at_hash`(access token hash)が含まれており、これによってクライアントは認可コードやアクセストークンの置き換えを防ぐことができます。

>　The c_hash in the ID Token enables Clients to prevent Authorization Code substitution. The at_hash in the ID Token enables Clients to prevent Access Token substitution.
> [Final: OpenID Connect Core 1.0 incorporating errata set 2](https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken) より

横取りの英語は`interception`ですが、`substitution`という言葉が使われていることに注意してください。

置き換えとは、攻撃者が事前に正常にログイン手順を踏んで取得した認可コードやアクセストークンを、被害者のものにすり替えることです。
横取りとは、攻撃者が通信経路上で認可コードやアクセストークンを盗み見ることです。

以下では、置き換えの問題について具体的なシナリオを考えます（参考：https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-threatmodel-08#section-4.4.1.13 )。

この例では、攻撃者は悪意のあるアプリを使って、被害者の認可コードを取得し、ターゲットアプリにログインすることで、被害者のアカウントデータにアクセスをすることを考えます。
まずは、OIDCを使わない場合のシナリオを考えます。

```mermaid
sequenceDiagram
    participant Victim as 被害者
    participant Attacker as 攻撃者
    participant MaliciousApp as 悪意のあるアプリ
    participant TargetApp as ターゲットアプリ
    participant Provider as 認証プロバイダー<br/>(リソースサーバーを含む)

    Attacker ->> TargetApp: ターゲットアプリにアクセスをし、攻撃者のアカウントでログイン
    Victim->>MaliciousApp: ログインページにアクセス
    MaliciousApp->>Victim: ソーシャルログインボタンを表示
    Victim->>MaliciousApp: ソーシャルログインボタンをクリック
    MaliciousApp->>Provider: 認可リクエストを送信
    Provider->>Victim: 認証画面を表示
    Victim->>Provider: 認証情報を入力
    Provider->>Provider: 被害者を認証
    Provider->>MaliciousApp: 認可コードを返却

    note over MaliciousApp,Attacker: 認可コードを攻撃者に転送
    MaliciousApp-->>Attacker: 被害者の認可コード

    note over Attacker: 攻撃者がターゲットアプリでログインを進める
    Attacker->>TargetApp: ソーシャルログインを開始<br>(攻撃者のアカウント情報)
    TargetApp->>Provider: 認可リクエストを送信
    Provider->>Attacker: 攻撃者の認可コードを発行
    Attacker->>TargetApp: 認可レスポンスを送信 (正規の認可コード)

    note over Attacker: 認可コードを被害者のコードにすり替える
    Attacker->>TargetApp: 被害者の認可コードを送信
    TargetApp->>Provider: 被害者の認可コードでトークンリクエスト
    Provider->>TargetApp: アクセストークンを発行
    TargetApp->>Provider: ユーザー情報をクエリ
    Provider-->>TargetApp: 被害者の識別情報を返却

    note over TargetApp: 被害者としてログイン成功
    TargetApp-->>Attacker: 被害者のアカウントデータにアクセス可能
```

しかし、上記のシナリオはOIDCを利用すると防ぐことができます。

```mermaid
sequenceDiagram
    participant Victim as 被害者
    participant Attacker as 攻撃者
    participant MaliciousApp as 悪意のあるアプリ
    participant TargetApp as ターゲットアプリ
    participant Provider as 認証プロバイダー<br/>(リソースサーバーを含む)

    Attacker ->> TargetApp: ★ ターゲットアプリにアクセスをし、攻撃者のアカウントでログイン
    Victim->>MaliciousApp: ログインページにアクセス
    MaliciousApp->>Victim: ソーシャルログインボタンを表示
    Victim->>MaliciousApp: ソーシャルログインボタンをクリック
    MaliciousApp->>Provider: 認可リクエストを送信
    Provider->>Victim: 認証画面を表示
    Victim->>Provider: 認証情報を入力
    Provider->>Provider: 被害者を認証
    Provider->>MaliciousApp: 認可コードを返却

    note over MaliciousApp,Attacker: 認可コードを攻撃者に転送
    MaliciousApp-->>Attacker: 被害者の認可コード

    note over Attacker: 攻撃者がターゲットアプリでログインを進める
    Attacker->>TargetApp: ソーシャルログインを開始<br>(攻撃者のアカウント情報)
    TargetApp->>Provider: 認可リクエストを送信
    Provider->>Attacker: 攻撃者の認可コードを発行
    Attacker->>TargetApp: 認可レスポンスを送信 (正規の認可コード)

    note over Attacker: 認可コードを被害者のコードにすり替える
    Attacker->>TargetApp: 被害者の認可コードを送信
    TargetApp->>Provider: 被害者の認可コードでトークンリクエスト
    Provider->>TargetApp: アクセストークンとIDトークンを発行
    TargetApp->>TargetApp: IDトークンを検証
    note over TargetApp: IDトークン検証に失敗（不正な認可コード）
    TargetApp-->>Attacker: ログイン失敗（攻撃者のアクセス拒否）
```

アプリ上で行うIDトークン検証の具体的な方法は、IDトークンの`c_hash`や`at_hash`, `nonce`を使って、認可コードやアクセストークンが正しいものであるかを確認することです。
認可コードが正しいものという意味は、`sub`が正しいものであることを確認することです。
今回のケースでは、認可コードを発行したのは攻撃者ではなく被害者であるため、
上記の図の(★)で発行したIDトークンと、被害者の認可コードを使って発行したIDトークンを比較し、`c_hash`や`at_hash`を使って認可コードやアクセストークンが正しいものであるかを確認することで、攻撃者のアクセスを拒否することができます。

> sub
>   REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII [RFC20] characters in length. The sub value is a case-sensitive string.
> https://openid.net/specs/openid-connect-core-1_0.html?utm_source=chatgpt.com#IDToken より
> In OpenID Connect, this is mitigated through mechanisms provided through the ID Token. The ID Token is a signed security token that provides Claims such as iss (issuer), sub (subject), aud (audience), at_hash (access token hash), and c_hash (code hash). Using the ID Token, the Client is capable of detecting the Token Substitution Attack.
> https://openid.net/specs/openid-connect-core-1_0.html?utm_source=chatgpt.com#TokenSubstitution　より

## 5.1. 補足 : OIDCで防げない例

上記の例は、攻撃者が攻撃を開始する前にIDトークンを検証しているため、攻撃者のアクセスを拒否することができました。

しかし、IDトークンの検証が通ってしまうような以下のようなパターンはOIDCだけでは防げません。

1. 被害者がまだターゲットアプリを利用していない場合
    - 被害者のアカウントがターゲットアプリに登録されていない場合、攻撃者は被害者のアカウントを作成できてしまいます。
1. 攻撃者が初めてターゲットアプリにログインする場
    - 攻撃者が初めてターゲットアプリにログインする場合、IDトークンの不整合が起きないため、攻撃者のアクセスを拒否することができません。

そのため、OIDCに対しても、`nonce`や後述するような`PKCE`を使う必要があります（本記事では`nonce`については触れませんが、`nonce`はIDトークンの検証に使われるパラメータです。[OpenID Connectのstateとnonceの違いがわからなかった #openid_connect - Qiita](https://qiita.com/m28/items/10c3a1de1bdcfda874b1)の記事などが参考になるかと思います）。

# 6. OIDCを利用しないで、OAuth2.0の認証を安全に実装するには

OAuth2.0において、グラントタイプが `authorization_code`の場合はそもそも、認証コードを使ってアクセストークンを取得するため、アクセストークンはクライアント側には漏洩はされません。

> The authorization code provides a few important security benefits,
   such as the ability to authenticate the client, as well as the
   transmission of the access token directly to the client without
   passing it through the resource owner's user-agent and potentially
   exposing it to others, including the resource owner.
> https://datatracker.ietf.org/doc/html/rfc6749#section-1.3 より

そのため、認可コードの漏洩や置き換えに対して対策をすれば、OAuth2.0の認証を安全に実装することができます。

そこで利用できるのが、PKCE（[RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)）です。

PKCE(pixy)とは、認可コードフローを使用する際に、認可コードを取得する際に使用するランダムな文字列（`code_verifier`）を生成し、そのハッシュ値を認可リクエストに含めることで、認可コードの横取りを防ぐための仕組みです。

>  To mitigate this attack, this extension utilizes a dynamically
   created cryptographically random key called "code verifier".  A
   unique code verifier is created for every authorization request, and
   its transformed value, called "code challenge", is sent to the
   authorization server to obtain the authorization code.  The
   authorization code obtained is then sent to the token endpoint with
   the "code verifier", and the server compares it with the previously
   received request code so that it can perform the proof of possession
   of the "code verifier" by the client.  This works as the mitigation
   since the attacker would not know this one-time key, since it is sent
   over TLS and cannot be intercepted.
> https://datatracker.ietf.org/doc/html/rfc7636#section-1 より

(★マークがついている部分がPKCEの特徴です)
```mermaid
sequenceDiagram
    participant User as ユーザー
    participant App as Webアプリケーション
    participant Provider as ソーシャルログインプロバイダー<br/>(GoogleやFacebook)

    User->>App: ログインページにアクセス
    App->>User: ソーシャルログインボタンを表示
    User->>App: ソーシャルログインボタンをクリック
    App->>App: ★ランダムな文字列（code_verifier）を生成
    App->>App:★ code_verifierをハッシュ化してcode_challengeを生成
    App->>Provider: ★認可リクエストを送信<br/>(code_challengeとcode_challenge_methodを含む)
    Provider->>User: 認証画面を表示
    User->>Provider: 認証情報を入力
    Provider->>Provider: ユーザーを認証
    Provider->>User: 認可コードを含むレスポンス
    User->>App: 認可コードを持ちリダイレクト
    App->>Provider: ★認可コードとcode_verifierでアクセストークンをリクエスト
    Provider->>Provider: ★code_verifierを検証
    Provider->>App: アクセストークンを返却
    App->>Provider: アクセストークンでユーザー情報をリクエスト
    Provider->>App: ユーザー情報を返却
    App->>User: ログイン成功、アカウント情報を表示
```
参考
1. [Authorization Code Flow with Proof Key for Code Exchange (PKCE)](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce)

具体的なパラメータとしては、`code_challenge`と`code_verifier`などがあります。
> https://datatracker.ietf.org/doc/html/rfc7636#section-6.1 より

こちらを使うことで、認可コードの横取りを防ぐことができます。

また、この方法は認可コードの置き換えにも対応しています。

というのも、認可コードを発行する際の`code_challenge`は、`code_verifier`をハッシュ化したものであり、`code_verifier`はランダムな文字列であるため、攻撃者が認可コードを盗んでも、`code_verifier`を知らない限り、アクセストークンを取得することができないためです。

:::note warn
OIDCでも認可コードの横取りなどについて対策が必要です。詳細は、[OAuth・OIDCへの攻撃と対策を整理して理解できる本（リダイレクトへの攻撃編 [2023年改訂版] - Auth屋 - BOOTH](https://booth.pm/ja/items/1877818)などをご参照ください。
:::

よって、OIDCを使わなくても、OAuth2.0の認証を安全に実装することができます。

:::note alert 
ただし、`code_verifier`は認証用途に作られていないにも関わらず、本記事は認証用途にも利用をしています。ここに対して、本当に問題ないかは十分考える必要があり、OIDC等の認証のためのフレームワークを利用した方が安全と考えています。
:::

# 7. まとめ

OIDCを使わなくてもPKCEを使うことで安全に認証を実装することができます。

ただし、idトークンを利用した認証が行えるOIDCを利用することが、業界のデファクトスタンダードです。そのため、プロバイダー側でOIDCが提供されている場合はそちらを利用することを推奨します（その場合もPKCEなどの対策は必要です）。


# 8. 感想

確かにOAuth2.0は認可のプロトコルであるため、私も認証に使うのは危険だと思っていましたが、PKCEを使うことで安全に認証を実装できると2024/11/23現在は判断しています。一方で、業界のデファクトなどを踏まえると、OIDCを使える場面ではそちらを利用して行きたいと考えています。

# 9. 謝辞
ritouさんコメントありがとうございます🙇

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">&gt; OIDCを使わなくてもPKCEを使うことで安全に認証を実装することができます<br><br>OIDCを使えるなら使いましょう、そうじゃない場合は(IdP/Client双方で)よく考えて実装しましょう、にならないと厳しい<br><br>OAuth2.0によるソーシャルログインは危険ではない~PKCE,OIDCの話を添えて~ <a href="https://t.co/a03rv398kQ">https://t.co/a03rv398kQ</a></p>&mdash; 👹秋田の猫🐱 (@ritou) <a href="https://twitter.com/ritou/status/1867913911544287594?ref_src=twsrc%5Etfw">December 14, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>