こんにちは!セキュリティの世界へようこそ。
初めてWebアプリやAPIの認証まわりを触る方にとって、「OAuth 2.0」とか「認可コード」といった言葉は、ちょっと呪文のように聞こえて難しく感じてしまいますよね。でも、安心してください。一歩ずつ、身近な例えから紐解いていけば、誰でもしっかりと理解できるようになりますよ。
今回は、OAuth 2.0の仕組みの裏に潜むちょっと怖い落とし穴、「認可コード横取り攻撃(Authorization Code Interception)」について、泥棒と合鍵の防犯に例えながら優しく解説していきます。実務で使える対策コードも用意しましたので、一緒に学んでいきましょう!
—
1. 例え話でスッキリ理解!OAuth 2.0と「合鍵」の仕組み
まずは、OAuth 2.0(オーオーアス ニーテンゼロ)が普段どんなことをしているのか、おうちのセキュリティに例えてみますね。
あなたが「写真共有アプリ(スマホアプリ)」を使っていて、そこに「クラウドストレージ(別のサービス)」の写真を表示させたいとします。
ここで普通なら、「クラウドストレージのパスワード」を写真アプリに教えちゃいそうになりますよね。でも、これだと写真アプリを作る会社が悪人だったら、あなたの他のデータまで全部見られてしまって危険です。
そこで登場するのが、OAuth 2.0という仕組みです。これは、いわば「我が家の玄関の鍵は渡さず、『写真を見るための専用の一時パス(期間限定の合鍵)』だけを渡す仕組み」になります。
泥棒が狙うのは「合鍵を受け取る瞬間」
この仕組みを悪用しようとするサイバー攻撃者(泥棒)は、まさにその「合鍵(認可コード)」が家主(ユーザー)からアプリへと手渡される瞬間を狙っています。
お父さん(ユーザー)が、「はい、この合鍵を使ってね」とアプリに鍵を渡そうとしたとき、もし不審な偽の郵便受け(不正なリダイレクト先)があったらどうでしょう?
うっかりその偽の郵便受けに合鍵を入れてしまうと、泥棒にその鍵がそっくりそのまま盗まれてしまいますよね。これが、今回解説する「認可コード横取り攻撃」の正体です。
—
2. 認可コード横取り攻撃のメカニズム
では、もう少し技術寄りの言葉で、この攻撃がどうやって起こるのかを覗いてみましょう。
スマートフォンアプリなどがOAuth 2.0を使ってログインする際、大まかに次のようなステップを踏みます。
1. 認可リクエスト: アプリがユーザーをブラウザ経由で認証サーバー(GoogleやLINEなど)に飛ばす。
2. 認可コードの発行: ユーザーがログインして「許可する」と押すと、認証サーバーはアプリへ向かって「認可コード」という小さなチケットを発行する。
3. リダイレクト: 認証コードを抱えたブラウザが、アプリがあらかじめ指定した宛先(リダイレクトURI)に戻ってくる。
4. トークン交換: アプリはその認可コードを裏側で認証サーバーに渡し、「本物のアクセストークン(真の合鍵)」と交換する。
どこに罠があるの?
問題はステップ3の「リダイレクト」です。スマホアプリの場合、アプリ専用の独自のURLスキーム(例: myphotoapp://callback など)を使って、ブラウザからアプリへと画面を戻します。
もし、このURLスキームを悪意ある別のアプリがスマホ内で勝手に「私のアプリもこの合鍵を受け取る住所として登録しちゃおう!」と乗っ取っていたらどうなるでしょうか?
ユーザーが「許可する」を押した瞬間、本来のアプリではなく、攻撃者のアプリに認可コードがピュッと飛んでいってしまうのです。
攻撃者はそのコードを手に入れれば、あとは自分のものとしてアクセストークンに変換し、ユーザーになりすましてアカウントを乗っ取ることができてしまいます。怖いですね……!
—
3. 救世主登場!「PKCE(ピクシー)」という最強の防犯ロック
「じゃあスマホアプリでのログインなんて怖くて作れないよ!」と思ったそこのあなた、安心してください。この問題をスパッと解決する現代のスタンダードな防御策が用意されています。
それがPKCE(Proof Key for Code Exchange:ピーケーシーイー、通称ピクシー)と呼ばれる仕組みです。
鍵の受渡しの秘密の合言葉
PKCEの考え方はとてもシンプルです。先ほどの例えで言うと、合鍵を渡すときに「2人だけの秘密の合言葉(コード・ベリファイアー)」をあらかじめ決めておく方法です。
1. アプリがリクエストを送るとき、ランダムな文字列(合言葉)を作り、その「ハッシュ値(暗号化して形を変えたもの)」だけを認証サーバーにこっそり伝えます(「この合言葉を知っている人だけに鍵を渡してね」と約束します)。
2. 認証コードが横取りされて、攻撃者がそれを手に入れたとします。
3. しかし、攻撃者は肝心の「元の合言葉(コード・ベリファイアー)」を知りません。
4. 認証サーバーは、「合言葉の証明ができないから、このコードは渡さないよ!」と突っ返すことができます。
これで、仮に途中で認可コードが盗聴されたとしても、中身を使わせないようにガチガチにガードできるというわけです。
—
4. 実務で実装する際のコード例と設定ポイント
それでは、ここからは実際の開発現場でどのようにPKCEを組み込むのか、具体的な設定やコードを見ていきましょう。一歩ずつ実装すれば難しくありません!
① リクエスト送信時のパラメータ設定(フロントエンド / モバイルアプリ)
OAuth 2.0の認可リクエストを送る際、通常のパラメータに加えてPKCE用のパラメータを付与します。以下はJavaScript(WebやReact Native等)のイメージコードです。
// ランダムな文字列(コード・ベリファイアー)を生成する関数
function generateCodeVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
// URLセーフなBase64エンコードに変換
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// ベリファイアーからチャレンジ(ハッシュ値)を生成する関数(SHA-256)
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// 認証フローの開始時に実行する処理
async function startOAuthFlow() {
const codeVerifier = generateCodeVerifier();
// 後でトークン交換の時に使うので、安全なストレージに一時保存しておく
sessionStorage.setItem('code_verifier', codeVerifier);
const codeChallenge = await generateCodeChallenge(codeVerifier);
// 認証サーバーへのリクエストURLを組み立てる
const authEndpoint = 'https://auth.example.com/oauth/authorize';
const clientId = 'your_client_id_12345';
const redirectUri = 'myphotoapp://callback'; // アプリのカスタムURLスキーム
const authUrl = `${authEndpoint}?response_type=code` +
`&client_id=${encodeURIComponent(clientId)}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&code_challenge=${encodeURIComponent(codeChallenge)}` +
`&code_challenge_method=SHA-256`; // PKCEの方式を指定
// ユーザーを認証画面へリダイレクト
window.location.href = authUrl;
}
- ポイント:
code_challengeとcode_challenge_method=SHA-256を必ず含めるようにしましょう。これによってサーバー側に「PKCEを使うよ」と宣言します。
—
② トークン交換時のパラメータ設定(バックエンド / アプリ内処理)
ユーザーが無事にログインを済ませ、アプリへ「認可コード」が戻ってきたら、今度はバックエンド(またはアプリの通信処理)でアクセストークンに交換します。このとき、最初に作った生の状態の合言葉(code_verifier)を一緒に送信するのがポイントです。
以下はPHPを使ったバックエンドでのトークン交換のサンプルコードです。
<?php
// リダイレクト先で受け取った認可コードと、事前に保存しておいたベリファイアーを取得
$authorizationCode = $_GET['code'] ?? '';
$codeVerifier = $_SESSION['code_verifier'] ?? ''; // 保存しておいた生の合言葉
if (empty($authorizationCode) || empty($codeVerifier)) {
die('必要なパラメーターが不足しています。');
}
// 認証サーバーのトークンエンドポイント
$tokenEndpoint = 'https://auth.example.com/oauth/token';
$postData = [
'grant_type' => 'authorization_code',
'client_id' => 'your_client_id_12345',
'code' => $authorizationCode,
'redirect_uri' => 'myphotoapp://callback',
'code_verifier' => $codeVerifier // ここで生の合言葉を送信し、サーバー側で検証してもらう
];
// cURLを使って認証サーバーへPOSTリクエストを送信
$ch = curl_init($tokenEndpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
$response = curl_exec($ch);
curl_close($ch);
$tokenData = json_decode($response, true);
if (isset($tokenData['access_token'])) {
// 無事にアクセストークンの取得に成功!
$accessToken = $tokenData['access_token'];
echo "アクセストークンの取得に成功しました!安全にAPIを利用できます。";
} else {
// 認証エラーや検証失敗
echo "トークンの取得に失敗しました。不正なリクエストの可能性があります。";
}
- ポイント: サーバー側は、最初に受け取った
code_challengeと、今回送られてきたcode_verifierをハッシュ化して一致するかをチェックします。ここで一致して初めてアクセストークンが発行されるため、万が一認可コードが途中で盗まれても攻撃者は先に進めなくなります。
—
5. まとめ:安全なアプリケーション開発のために
今回はOAuth 2.0の認可コード横取り攻撃と、それを防ぐための強力な武器「PKCE」について解説しました。
- 認可コードは、スマホのURLスキームなどを通じて悪意あるアプリに横取りされるリスクがある。
- 特にネイティブアプリやSPA(シングルページアプリケーション)では、PKCEの導入が事実上の必須要件となっている。
- 実装の際は
code_challengeとcode_verifierを正しくペアで扱い、サーバー側できっちり検証させる。
セキュリティの仕組みは最初は複雑に感じるかもしれませんが、「誰がどこで鍵を持っているか」を意識するだけで、グッと理解しやすくなります。ぜひご自身の開発するアプリやインフラでも、PKCEが正しく有効化されているか確認してみてくださいね。一歩ずつ、安全で強固なシステムを作っていきましょう!
コメント