コース目次 / 第4章

OAuth / OIDC — 認可コードフローと、その綻び

自作のモックIdPで認可コードフローを追い、redirect_uriの甘い検証で認可コードを攻撃者に渡せてしまう穴を再現します。stateの役割(CSRF防止)も押さえ、厳格な検証で直します。

第4章 / 全6章目安 約14分この章のゴール: 認可コードフローを理解し、redirect_uri検証とstateの綻びを診断できるようになる

「Google でログイン」のような、別のサービスの認証を借りる仕組みが OAuth 2.0(と、その上の認証層 OIDC)です。複数のサービスがまたがるぶん、綻びどころも独特です。ここでは自作のモックで、最も基本の認可コードフローを追い、代表的な穴を1つ、手を動かして見ます。

くり返します——相手はあなたが立てる node のモックだけ。実在の ID プロバイダ(Google 等)に診断行為を向けることはしません(法と倫理)。仕組みと綻びは、モックで完全に学べます。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

登場人物と、流れ

利用者(あなた)、クライアント(ログインさせたいアプリ。例: はるとの旅アプリ)、認可サーバ / IdP(認証を提供する側。例: Google)。流れはこう:
1. クライアントが、利用者を IdP の /authorize へ送る(「この人を認証して」)。
2. IdP が利用者を認証し、認可コードを付けて、クライアントの redirect_uri へ返す。
3. クライアントが、そのコードを IdP の /token で access token に交換する(裏側の通信)。
4. クライアントが token で /userinfo を引き、「この人は haruto」と分かってログイン成立。
コード(表を通る)と token(裏で交換)を分けるのが「認可コードフロー」の肝。トークンそのものを URL に載せないぶん安全です。

モックの IdP を立てる

/authorize が redirect_uri をどう検証するかが、この章の焦点です。mock-idp.js として保存します。STRICT=1 で厳格版です。

// mock-idp.js — 認可サーバのモック。127.0.0.1:5000。STRICT=1 で redirect_uri を厳格検証。
const http = require('node:http');
const crypto = require('node:crypto');

const STRICT = process.env.STRICT === '1';
// クライアント登録: このクライアントに許す redirect_uri は、この1つだけ。
const REGISTERED = 'http://127.0.0.1:5001/callback';
const codes = new Map();

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:5000');

  if (url.pathname === '/authorize') {
    const redirectUri = url.searchParams.get('redirect_uri') || '';
    const state = url.searchParams.get('state') || '';
    // 【穴】STRICT でないと、登録済み redirect_uri と照合せず、来た値へそのまま返す。
    if (STRICT && redirectUri !== REGISTERED) {
      res.writeHead(400); res.end('redirect_uri が登録と一致しません'); return;
    }
    // 利用者を認証したことにして、認可コードを発行(本来はここでログイン画面)。
    const code = crypto.randomBytes(8).toString('hex');
    codes.set(code, 'haruto');
    // コードを付けて redirect_uri へ返す(state もそのまま返す)。
    const back = redirectUri + '?code=' + code + '&state=' + encodeURIComponent(state);
    res.writeHead(302, { Location: back }); res.end();
    return;
  }

  if (url.pathname === '/token' && req.method === 'POST') {
    let body = ''; req.on('data', (c) => (body += c));
    req.on('end', () => {
      const code = new URLSearchParams(body).get('code');
      const user = codes.get(code);
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify(user ? { access_token: 'tok-' + user } : { error: 'bad code' }));
    });
    return;
  }

  res.end('idp');
}).listen(5000, '127.0.0.1', () => console.log('モックIdP: http://127.0.0.1:5000 (Ctrl+C で停止)'));

攻撃 — redirect_uri を、攻撃者へ向ける

まず脆弱版(照合しない)で起動します。

node mock-idp.js

正規のクライアントは redirect_uri=http://127.0.0.1:5001/callback を使います。ところが、/authorize が値を照合しないなら、攻撃者は自分のサイトを redirect_uri に指定できます。

curl -i "http://127.0.0.1:5000/authorize?client_id=demo&redirect_uri=http://127.0.0.1:5999/steal&state=abc"

レスポンスは 302、その Location は http://127.0.0.1:5999/steal?code=…——認可コードが、攻撃者の指定した場所(:5999)へ送られています。攻撃者が被害者にこの /authorize リンクを踏ませれば、被害者の認可コードが攻撃者のサーバに届き、攻撃者がそれを token に交換して被害者としてログインできてしまう。redirect_uri の甘い検証は、認可コードの横取りに直結します。

修正 — redirect_uri は、登録済みと完全一致

直しは、IdP が、クライアント登録時の redirect_uri と完全一致でしか受け付けないこと。STRICT=1 で再起動します。

STRICT=1 node mock-idp.js
curl -i "http://127.0.0.1:5000/authorize?client_id=demo&redirect_uri=http://127.0.0.1:5999/steal&state=abc"
# => 400 redirect_uri が登録と一致しません

攻撃者のURLは弾かれ、登録済みの :5001/callback だけが通ります。前方一致や部分一致では不十分(…/callback.attacker.example.com のような迂回がある)で、完全一致の許可リストが原則です。これも、これまでと同じ「許可リスト」の発想です。

もう一つの綻び — state を確かめる

/authorize に付けた state パラメータには、役割があります。

state は、CSRF(なりすましログイン)を防ぐための合言葉です。クライアントは、フローを始めるとき推測できない state を生成して保存し、/authorize に付けます。IdP はそれをそのまま redirect_uri に返す。クライアントは /callback で、返ってきた state が自分の保存したものと一致するかを確かめる。
もしクライアントが state を確かめないと——攻撃者が自分の認可コードを被害者のブラウザに注入し、被害者を攻撃者のアカウントでログインさせる(ログイン CSRF)ことができてしまう。以後、被害者が入力する情報が攻撃者のアカウントに溜まる、という被害に。クライアントは、必ず state を発行し・照合する。

診断では、OAuth のクライアント側で state の有無と照合、IdP 側で redirect_uri の完全一致を、必ず確認します。この2つが、認可コードフローの二大チェックポイントです。

持ち帰る一言

redirect_uri は完全一致、state は必ず照合。 認可コードフローは、コードを表・トークンを裏で扱う堅い仕組みですが、redirect_uri の甘い検証はコード横取りに、state の未照合はなりすましログインに直結する。どちらも「許可リスト」と「意図の確認」——このコースを貫く原則です。次はまとめて、認証・セッションの診断を締めます。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。