【実務・中級編】 OAuth 2.0認可コードフローにおけるPKCEの必須化と実装 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

OAuth 2.0の「認可コード横取り」をPKCEで封殺する:エンジニアが知るべき実装の急所

現場でコードを書いていると、「とりあえず動く」認証フローを組むことに注力しがちだが、セキュリティチーフとして一つだけ断言しておく。PKCE(Proof Key for Code Exchange)なしの認可コードフローは、現代のWebアプリケーションにおいて「鍵のかかっていない玄関」と同じだ。

今回は、なぜPKCEが必須なのか、その攻撃手法と、明日から現場で使える実装パターンを叩き込む。

—

1. なぜPKCEが必要なのか:攻撃者の視点から見る脆弱性

従来の認可コードフロー(Authorization Code Grant)は、サーバーサイドアプリケーションでは強力だが、モバイルアプリやSPA(シングルページアプリケーション)では致命的な脆弱性を抱えている。

認可コード横取り攻撃 (Authorization Code Interception Attack) のPoC

攻撃者は、OSの「カスタムURLスキーム」を悪用する。

1. 悪意あるアプリのインストール: 攻撃者が作成した不正アプリをユーザーにインストールさせる。
2. 認可リクエストの傍受: ユーザーが正当なアプリでログインしようとした際、攻撃者のアプリが同じカスタムURLスキーム(例: myapp://callback)を登録し、OSが発行した「認可コード(Code)」を横取りする。
3. トークン交換: 攻撃者はそのコードをIDP(認可サーバー)に提示し、アクセストークンを奪取する。

これを防ぐのがPKCEだ。「最初に送ったリクエストと、後にコードを交換するリクエストが同一のクライアントであること」を、暗号学的に証明する。

—

2. PKCEの仕組み:秘密の「使い捨てパスワード」

PKCEは、以下の二つの要素を使ってリクエストを検証する。

1. Code Verifier: クライアントが生成するランダムな文字列(秘密の値)。
2. Code Challenge: Verifierをハッシュ化(通常はSHA-256)して変換したもの。

認可リクエスト時に Code Challenge を送信し、トークン交換時に Code Verifier を送信する。IDPは、受け取った Verifier をハッシュ化し、最初に受け取った Challenge と一致するか確認する。攻撃者が Verifier を知らなければ、コードを交換することはできない。

—

3. 【実務向け】PKCEの実装サンプル

ここでは、フロントエンド(JavaScript)で Code Verifier を生成し、検証を準備するまでの核心部分を解説する。

Step 1: Code Verifier と Challenge の生成

// 乱数生成器でVerifierを作成 (43〜128文字のランダム文字列)
function generateCodeVerifier() {
    const array = new Uint8Array(32);
    window.crypto.getRandomValues(array);
    return base64UrlEncode(array);
}

// SHA-256でハッシュ化し、Code Challengeを作成
async function generateCodeChallenge(verifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const digest = await window.crypto.subtle.digest('SHA-256', data);
    return base64UrlEncode(new Uint8Array(digest));
}

// Base64URLエンコード(パディング除去)
function base64UrlEncode(buffer) {
    return btoa(String.fromCharCode.apply(null, buffer))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

Step 2: 認可リクエストの送信(パラメータ付与)

認可サーバーへのリクエストURLを構築する際、以下のパラメータを必ず含める。

const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);

// セッションストレージ等にVerifierを保存(トークン交換時に必要)
sessionStorage.setItem('pkce_verifier', verifier);

const authUrl = `https://idp.example.com/authorize?` +
    `response_type=code` +
    `&client_id=YOUR_CLIENT_ID` +
    `&redirect_uri=YOUR_CALLBACK_URL` +
    `&code_challenge=${challenge}` + // これがPKCEの鍵
    `&code_challenge_method=S256`;   // SHA-256を指定

window.location.href = authUrl;

—

4. セキュリティチーフからの「泥臭い」アドバイス

PKCEを実装する際、多くのエンジニアが躓くポイントが二つある。

1. Verifierのライフサイクル: Code Verifier は、トークン交換が完了するまで確実にセッションストレージ等で保持する必要がある。タブを閉じたり、ブラウザをリロードしても消えない設計、あるいはメモリ上のグローバル変数で安全に管理する設計を徹底してほしい。
2. IDP側の設定確認: AWS Cognito、Auth0、あるいは自前のIdentityServerを使うにしても、「PKCE必須(Require PKCE)」の設定がオンになっているか必ず確認すること。コード側だけPKCEに対応しても、サーバー側が古い仕様を受け入れていれば、脆弱性は残ったままだ。

Nginxでの防御的設定(おまけ)

Webアプリケーションの入り口であるNginxでは、認可リクエストが不正なパラメータで送られてこないよう、バリデーションをかけることも重要だ。

# 認可リクエストのクエリパラメータを厳格にチェックする例
location /authorize {
    if ($arg_code_challenge_method != "S256") {
        return 403; # PKCEなしの古いリクエストは門前払い
    }
    # その他のプロキシ設定...
}

—

最後に:セキュリティは「積み重ね」だ

PKCEは銀の弾丸ではない。これだけで全ての攻撃を防げるわけではなく、あくまで「認可コード横取り」に対する強力な防壁だ。

しかし、こうした「面倒な実装」を一つ一つ徹底できるかどうかが、重大インシデントを起こすエンジニアと、それを未然に防ぐエンジニアの分かれ道になる。「セキュリティは機能の一つ」と捉え、今日からあなたのプロダクトの認証フローを見直してほしい。

もし実装で詰まったら、仕様書(RFC 7636)の原典に戻ることを忘れるな。それが一番の近道だ。

コメント

タイトルとURLをコピーしました