【入門編】 API認証におけるOAuth 2.0とOpenID Connectの安全な実装 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

こんにちは!新人エンジニアの皆さん、そしてセキュリティの勉強を始めたばかりの皆さん、日々の開発やお疲れ様です。

システムを作っていると、必ずと言っていいほど直面するのが「ログイン機能」や「APIの安全な呼び出し」ですよね。「ユーザーが本当に本人か」「他の人にデータを勝手に盗まれないか」を考えるのは、まるで大切な家を守る警備システムを設計するようなもの。最初は難しく感じるかもしれませんが、一歩ずつ安全な仕組みを紐解いていけば、必ず理解できるようになりますよ。

今回は、現代のWeb開発で必須の知識となっている「API認証におけるOAuth 2.0とOpenID Connect(OIDC)」について、家の防犯に例えながら優しく解説していきます。実務ですぐに使えるコード例も用意したので、一緒にマスターしていきましょう!

—

1. 家の鍵で例える「OAuth 2.0」と「OpenID Connect」

セキュリティの話をするとき、よく使われるのが「合鍵」と「身分証明書」の例えです。

  • OAuth 2.0(オーオーエス・ニーテンゼロ)は「合鍵(アクセストークン)」
  • 例えば、家事代行サービスの人に「リビングだけに入ってもいいよ」という期間・場所限定のデジタル合鍵を渡すイメージです。家全体(サーバーの全データ)の鍵ではなく、限られた部屋(特定のAPI)だけに出入りするための「認可」の仕組みになります。
  • OpenID Connect(オーアイディシー)は「身分証明書(IDトークン)」
  • こちらは「あなたは間違いなく〇〇さんですね」と証明するためのパスポートや運転免許証のようなものです。「認証」の役割を果たします。

この2つを組み合わせて、「私は〇〇です(OIDC)」と証明した上で、「このアプリにデータの読み書きを許可します(OAuth 2.0)」という安全なやり取りを実現するのが現代のスタンダードです。

—

2. 攻撃者が狙う盲点:なぜ「暗黙のフロー」はダメなのか?

昔のWebやスマホアプリでは、ログインした瞬間に直接合鍵(アクセストークン)をバーンと渡してしまう「インプリシット(暗黙の)フロー」というやり方がよく使われていました。

しかし、これは例えるなら、「合鍵を大きく印刷した紙を、街中に貼り出して持ち歩く」ようなもの。もしアプリの隙(脆弱性)を突かれて、URLのフラグメント(#access_token=...の部分)がブラウザの履歴や不正なスクリプトに覗き見されたら、一瞬で合鍵が盗まれてしまいますよね。

そこで登場するのが、今や業界の絶対的なルールとなっている「認可コードフロー + PKCE(ピーシーイー)」です。

—

3. 鉄壁のペア!「認可コードフロー with PKCE」の仕組み

PKCE(Proof Key for Code Exchange)と聞くと呪文のようですが、防犯に例えれば「合い言葉付きの金庫」です。

1. お出かけ前の仕込み(クライアント側)

  • アプリ側でランダムな秘密の文字列(コード・ベリファイアー)を作り、それをハッシュ化した「合言葉(コード・チャレンジ)」を認証サーバーにこっそり伝えます。

2. 合鍵ではなく「引き換え券」をもらう

  • ユーザーがログインに成功すると、サーバーは合鍵そのものではなく、一度きりしか使えない「引き換え券(認可コード)」をアプリに渡します。もしここで引き換え券が盗まれても、本物の合鍵には変えられません。

3. 秘密の合言葉で合鍵と交換する

  • アプリ側は、受け取った引き換え券と、最初に用意した「秘密の合言葉(コード・ベリファイアー)」をセットでサーバーに提示します。サーバーは「おっ、合言葉が一致したね」と確認して初めて、本物の合鍵(アクセストークン)を渡すのです。

この仕組みのおかげで、途中で引き換え券が盗み見られても、合言葉を知らない攻撃者は合鍵を手に入れることができません。

—

4. 実装の具体例:フロントエンドでのPKCEリクエスト構築

それでは、実際にJavaScriptを使って、安全なログイン要求(リクエスト)を組み立てるコードを見てみましょう。実務でそのままベースとして使えるようにコメントを丁寧に書いています。

// ランダムな文字列(コード・ベリファイアー)を生成する関数
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(/=+$/, '');
}

// コード・ベリファイアーからハッシュ値(コード・チャレンジ)を生成する関数
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 startLoginFlow() {
    // 1. 秘密の合言葉(ベリファイアー)を作成し、後で使うのでブラウザのセッションストレージに一時保存
    const codeVerifier = generateCodeVerifier();
    sessionStorage.setItem('code_verifier', codeVerifier);

    // 2. 合言葉からハッシュ化されたチャレンジを作成
    const codeChallenge = await generateCodeChallenge(codeVerifier);

    // 3. 認証サーバーのエンドポイントURL
    const authEndpoint = 'https://auth.example.com/oauth/authorize';
    const clientId = 'your_client_id_12345'; // あなたのアプリのID
    const redirectUri = 'https://myapp.com/callback'; // ログイン後に戻ってくるURL

    // 4. パラメータを組み立ててリダイレクト(引っ越し)する
    const authUrl = `${authEndpoint}?` +
        `response_type=code` + // 引き換え券(コード)を要求する指定
        `&client_id=${encodeURIComponent(clientId)}` +
        `&redirect_uri=${encodeURIComponent(redirectUri)}` +
        `&scope=openid profile api:read` + // 欲しい権限(OIDCのスコープ含む)
        `&code_challenge=${encodeURIComponent(codeChallenge)}` + // PKCEのチャレンジ
        `&code_challenge_method=S256`; // ハッシュアルゴリズムの指定

    // ユーザーを認証画面へ誘導
    window.location.href = authUrl;
}

—

5. アクセストークンの有効期限管理とリフレッシュトークンの安全な取り扱い

無事に合鍵(アクセストークン)を手に入れた後も、セキュリティ上の重要なポイントがあります。それは「鍵の寿命」です。

アクセストークンの有効期限は「短く」する

もしアクセストークンの有効期限を「1年」などに設定してしまうと、万が一その鍵が盗まれたとき、1年間ずっと侵入され放題になってしまいます。そのため、アクセストークンの寿命はあえて「15分〜1時間程度」と非常に短く設定します。

では、毎回ログインし直すの? そこで「リフレッシュトークン」

「15分で有効期限が切れたら、ユーザーが何度もパスワードを入れ直さないといけないのでは?」と思いますよね。そこで登場するのがリフレッシュトークン(身分更新カード)です。

  • アクセストークンが切れたら、アプリは裏側でリフレッシュトークンを認証サーバーに送り、「新しいアクセストークンをください」とこっそりお代わりを請求します。
  • これにより、ユーザーに面倒な再ログインを強いることなく、セキュリティの安全性を保つことができます。

リフレッシュトークンの保管場所に関する重大な注意点

ここが実務で最もミスしやすい泥臭いポイントです。リフレッシュトークンは、アクセストークンよりも強力な「新しい合鍵を生み出す元」です。

  • 絶対にやってはいけないこと: localStorageやsessionStorageなどのブラウザのストレージに保存すること。もしサイトにXSS(クロスサイトスクリプティング)という脆弱性があった場合、JavaScriptから簡単にリフレッシュトークンが盗まれ、悪意ある第三者に渡ってしまいます。
  • 推奨される対策:
  • SPA(シングルページアプリケーション)の場合: バックエンドのサーバー(BFF: Backend for Frontend)を挟み、リフレッシュトークンはJavaScriptから触れないHttpOnlyかつセキュアなCookieに保存します。
  • スマホアプリ(ネイティブアプリ)の場合: OSが提供する安全な領域(iOSならKeychain、AndroidならEncryptedSharedPreferences)に厳重に暗号化して保存します。

—

まとめ:安全な扉を開くために

今回は、OAuth 2.0とOpenID Connectの基本から、PKCEを使った安全なコードフロー、そしてトークンの有効期限や保存場所の注意点までを解説しました。

  • ログインやAPI連携には「認可コードフロー + PKCE」を使う。
  • アクセストークンは短命にし、リフレッシュトークンでスマートに更新する。
  • 強力なトークンはJavaScriptから触れない場所(HttpOnly CookieやOSの安全な領域)に隠す。

セキュリティの仕組みは、一見すると制約が多くて面倒くさいと感じるかもしれません。しかし、それはユーザーの大切なデータやプライバシーを守るための頑丈な盾となります。

一つひとつの仕組みの意味を理解すれば、自信を持って安全なシステムを設計できるようになります。焦らず、一歩ずつセキュアな開発を楽しんでいきましょう!

コメント

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