こんにちは!セキュリティの世界へようこそ。
初めてAPIの認証や認可(OAuth 2.0 / OIDC)の仕組みに触れるとき、アルファベットの専門用語がずらりと並んでいて「うっ…」と頭が痛くなってしまいますよね。
大丈夫です、一歩ずつ紐解いていけば決して難しくありません。今回は、最近の開発現場で非常によく見かける「OAuth 2.0 / OIDCの不適切な実装」について、身近な例えを交えながら、攻撃者がどこを狙ってくるのか、そしてどうやってそれを防げばいいのかを一緒に優しく学んでいきましょう!
—
1. 家の合鍵に例える「OAuth 2.0」と「OIDC」の基本
まずは、そもそもOAuth 2.0やOIDCって何なの?というところから整理しますね。
例えば、あなたの家(APIサーバー)に、お掃除ロボット(外部の連携アプリ)を呼ぶとします。お掃除ロボットに家の中を掃除してもらうためには、当然「玄関の鍵」を渡す必要がありますよね。
でも、本物の「マスターキー」をそのまま渡してしまうと、ロボットが壊れた時や、もし悪意ある人がロボットを乗っ取った時に大変なことになります。
そこで登場するのが「一時的な合鍵(アクセストークン)」です。
- OAuth 2.0 は、この「合鍵を安全に渡して、限定的な権限(リビングだけ入っていいよ、など)を与える仕組み」のことです。
- OIDC(OpenID Connect) は、その合鍵の仕組みをベースにして、「あ、あなたは間違いなく〇〇さんですね」という身分証明書(IDトークン)を発行する仕組みです。
便利な反面、この合鍵の渡し方や管理方法を少しでも間違えると、泥棒にやすやすと侵入を許してしまうことになります。ここからが本題のセキュリティの落とし穴です。
—
2. 攻撃者が狙う盲点①: state パラメータの欠如(CSRF攻撃)
OAuthの「認可コードフロー」という仕組みでは、ユーザーがログイン画面で「許可する」ボタンを押すと、外部アプリへ一度リダイレクト(画面の転送)されます。この時、攻撃者は「CSRF(クロスサイト・リクエスト・フォージェリ)」という手口を使って、あなたを罠にはめようとします。
身近な例えで考えてみましょう
あなたが友だちと遊園地の入場ゲート(認可サーバー)に並んでいます。
友だちが「先に入ってチケット交換してきてあげる!」とあなたのカバンから財布を持ち出そうとしますが、あなたは「ちょっと待って、本人確認のためにお互いの合言葉を決めよう」と言って、合言葉(state)を決めました。これなら、見知らぬ他人が友だちのふりをして近づいてきても、合言葉を知らないので追い払うことができますよね。
もし、この「合言葉(state)」をプログラムに実装し忘れているとどうなるでしょうか?
攻撃者は、あなたが別のサイトでログインしている隙に、自分が攻撃者のアカウントに紐づけた合鍵を、あなたのブラウザにこっそり送り込みます。あなたはそれに気づかず「ログイン完了!」となり、結果的に自分の個人情報や重要なデータが、攻撃者のアカウントと繋がってしまうのです。
対策: state パラメータを必ず使おう!
実装の際は、ランダムな文字列(セッションIDなど)を state パラメータとしてリクエストに含め、サーバー側に戻ってきた値と一致するか必ずチェックするようにしましょう。
// 【PHPでの実装例】認可リクエスト送信時
// 予測不可能なランダムな文字列を生成してセッションに保存します
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;
// 認可サーバーへリクエストを送るURLを作成
$auth_url = "https://example.com/oauth/authorize?";
$auth_url .= "response_type=code";
$auth_url .= "&client_id=YOUR_CLIENT_ID";
$auth_url .= "&redirect_uri=https://your-app.com/callback";
$auth_url .= "&state=" . $state; // ★ここで合言葉(state)を必ず含める!
// ユーザーを認可画面へ誘導
header("Location: " . $auth_url);
exit;
そして、コールバック(戻ってきたとき)の処理では以下のようにチェックします。
// 【PHPでの実装例】コールバック受診時の検証
session_start();
// 返ってきた state が、最初に保存したセッションの値と一致するか厳密に比較する
if (!isset($_GET['state']) || $_GET['state'] !== $_SESSION['oauth_state']) {
// 一致しない場合は、CSRF攻撃の可能性があるので処理を即座に中断!
die("セキュリティエラー: 不正なリクエストが検出されました。");
}
// 検証が成功したら、一時的なセッションの state は破棄する
unset($_SESSION['oauth_state']);
—
3. 攻撃者が狙う盲点②: リダイレクトURIの検証不備
認可コード(合鍵の引き換え券)をアプリに渡す際、あらかじめ決められた「戻り先(リダイレクトURI)」にだけ返す必要があります。しかし、このチェックが甘いと、攻撃者に合鍵をそっくりそのまま盗み取られてしまいます。
攻撃のメカニズム
もし開発者が redirect_uri のチェックを「https://your-app.com から始まっていれば何でもOK」というような雑な設定にしていると、攻撃者は次のような罠URLを作ります。
https://auth.example.com/authorize?client_id=123&redirect_uri=https://your-app.com.attacker-site.com/steal
これを見たユーザーは「あ、公式のアプリのURLだ」と勘違いしてログインボタンを押してしまいます。すると、本来ならあなたのアプリに戻るはずの「引き換え券(認可コード)」が、なんと攻撃者のサーバーへ直接送られてしまうのです!攻撃者はそのコードを使ってあなたの合鍵を手に入れ、あなたになりすましてシステムに侵入します。
対策: 完全一致(ホワイトリスト方式)で検証する
リダイレクトURIは、あいまいな前方一致や部分一致で判定しては絶対いけません。あらかじめ登録された完全なURL文字列(ホワイトリスト)と一字一句同じか厳密に比較しましょう。
// 【Node.js / Expressでの実装例】リダイレクトURIの厳密な比較
const allowedRedirectUris = [
"https://your-app.com/callback",
"https://your-app.com/auth/complete"
];
function validateRedirectUri(inputUri) {
// 配列の中に完全に一致するものが存在するかどうかをチェックする
return allowedRedirectUris.includes(inputUri);
}
// ルーティング処理
app.get('/callback', (req, res) => {
const redirectUri = req.query.redirect_uri;
if (!validateRedirectUri(redirectUri)) {
return res.status(400).send("無効なリダイレクトURIです。");
}
// 正常な処理を続行...
});
—
4. トークンの有効期限と「リフレッシュトークン」の安全な取り扱い
無事にログインできて合鍵(アクセストークン)を手に入れました。ここで気になるのが「この鍵、いつまで使えるの?」という有効期限の問題です。
鍵は長く持たせない、使い回さない
アクセストークンの有効期限が数ヶ月や数年など長すぎる設定になっていると、もしそのトークンがパソコンのログやブラウザの履歴から漏洩したとき、長期間にわたって不正アクセスされ放題になってしまいます。
そのため、アクセストークンの寿命はあえて「1時間」など短く設定するのが鉄則です。
「じゃあ、1時間ごとに毎回パスワードを入れ直すの面倒じゃない?」と思いましたよね?そこで登場するのが「リフレッシュトークン」です。
これは「新しい合鍵を発行してもらうための特別な引換券」のようなものです。
リフレッシュトークンの安全な保管場所
このリフレッシュトークンはアクセストークンよりも強い権力を持っています。なぜなら、これさえあれば何度でも新しい合鍵が作れてしまうからです。したがって、取り扱いには細心の注意が必要です。
- やってはいけないこと: JavaScriptから簡単にアクセスできる
localStorageやsessionStorageに保存すること。これだと、もしサイトにXSS(クロスサイト・スクリプト)脆弱性があった場合、一瞬でトークンが盗まれてしまいます。 - 推奨される対策: ブラウザから直接 JavaScript で読めないように、
HttpOnly属性およびSecure属性が付与されたCookie に保存し、SameSite設定も適切に行うことで、スクリプトからの不正な読み取りを完全にブロックします。
# 【HTTPレスポンスヘッダーの設定例】
Set-Cookie: refreshToken=abc123xyz_secure_token; Secure; HttpOnly; SameSite=Strict; Path=/api/token
Secure: 暗号化された通信(HTTPS)でのみCookieを送信する。HttpOnly: JavaScript(document.cookie等)からのアクセスを禁止し、万が一のXSSによる窃盗を防ぐ。SameSite=Strict: 他のサイトからの意図しないリクエストでCookieが送信されるのを防ぐ。
—
まとめ:一歩ずつ安全なシステムを作っていきましょう!
いかがでしたでしょうか?
OAuth 2.0やOIDCは、正しく使えば非常に強力で便利な仕組みですが、一歩実装を間違えると、玄関の鍵を全開にして泥棒を招き入れるような事態になってしまいます。
今日覚えた大切なポイントを振り返ってみましょう。
1. ログイン時の不正を防ぐために、state パラメータ(合言葉)を必ず生成して検証すること。
2. リダイレクト先はあいまいな判定にせず、完全一致のホワイトリスト方式で厳密にチェックすること。
3. トークンの寿命は短くし、リフレッシュトークンは HttpOnly Cookie で安全に管理すること。
セキュリティの対策は、一度にすべて完璧にやろうとすると大変です。「まずは state の実装を見直してみよう」「次はCookieの設定を確認してみよう」というように、一歩ずつ確実に進めていけば大丈夫です。
あなたの開発するシステムが、安全で信頼される素晴らしいものになるよう、これからも一緒に学んでいきましょう!
コメント