【入門編】 PKCE(Proof Key for Code Exchange)による認可コードフローの強化 – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!ネットワークの深淵とプロトコルの美しさを愛するインフラアーキテクトです。

今回は、モダンなWebアプリケーションやモバイルアプリの開発では避けて通れない、セキュリティの要「PKCE(Proof Key for Code Exchange:ピクシー)」について、じっくりと紐解いていきたいと思います。

「名前からして難しそう……」「OAuthとか認可コードとか、なんだか文字がいっぱいで頭がパンクしそう!」そんな風に思っていませんか? 一歩ずつ、身近な例えを交えながら優しく解説していきますので、どうぞリラックスしてついてきてくださいね!

—

1. なぜPKCEが必要なの? 〜スマホアプリが抱える「秘密を守れない」宿命〜

みなさんは、お気に入りのスマートフォンアプリや、ブラウザだけで動くSPA(シングルページアプリケーション)を普段からたくさん使っていることと思います。これらのアプリは、サーバー側にがっちり守られたシステムとは違い、「ユーザーの手元(クライアント側)」でコードが実行されるという大きな特徴を持っています。

ここで、従来のWebシステムで使われていた「認可コードフロー」の仕組みを少しだけ思い出してみましょう。
通常のWebアプリなら、サーバーの中に誰にも見られない「合い鍵(クライアントシークレット)」をこっそり隠しておくことができました。しかし、スマホアプリやブラウザのJavaScriptは、中身を頑張って解析しようと思えば、その合い鍵を丸裸にできてしまうんです。

郵便配達で例えてみましょう

ある重要書類(アクセストークン)を受け取るための「引換券(認可コード)」を、郵便受け(認可サーバー)から自分の家(アプリ)まで配達してもらう場面を想像してください。

通常のやり方だと、郵便受けから引換券を取り出した瞬間、悪意ある第三者(攻撃者)がそのスキを狙って「おい、その引換券をこっちによこせ!」と横取りしてしまう危険性があります。これが、いわゆる「認可コード横取り攻撃」です。

「スマホアプリには秘密の合い鍵を隠せないから、引換券を横取りされたらひとたまりもない……どうしよう!」
そんなピンチを華麗に救うヒーローこそが、今回学ぶPKCEなのです!

—

2. PKCEの仕組み 〜「自分だけの合言葉」で荷物を守る〜

PKCEがやってくれることは、実にシンプルでスマートです。一言で言うと、「配達される引換券に、自分だけのヒミツの鍵をかけて、本人しか開けられないようにする」という仕組みになります。

具体的には、以下の3つのステップで進んでいきます。

1. コードベリファイア(Verifier)の作成
アプリ側で、誰にも推測できないランダムな文字列(これがヒミツの合言葉になります)をこっそり作ります。これを「コードベリファイア」と呼びます。
2. コードチャレンジ(Challenge)の計算
その合言葉を、特定のルール(ハッシュ関数など)でグシャグシャとかき混ぜて、「コードチャレンジ」という変身後の姿を作ります。
3. 本人確認の証明
認可サーバーにお願いしに行くときは、元の合言葉ではなく変身後の姿(コードチャレンジ)だけを先に見せておきます。「後でこの合言葉の元の姿を当ててみせるから、引換券を私だけに発行してね!」と約束するわけです。

そして、最後に引換券を本物のトークンと交換する時、アプリは初めて元の合言葉(コードベリファイア)を認可サーバーに突き付けます。
認可サーバーは、「ふむ、最初に預かった変身後の姿と、今持ってきた元の合言葉がピタリと一致するね。君は本物の持ち主だ!」と確認して、ようやく安全にアクセストークンを渡してくれるのです。これなら、途中で引換券をチラ見されて横取りされても、合言葉を知らない攻撃者はトークンを手に入れることができませんよね!

—

3. 実装のステップをコードで見てみよう

「概念は分かったけれど、実際にどうやって書くの?」という方のために、Pythonを使った具体的なコード例を見てみましょう。難しく考えず、雰囲気を掴んでみてくださいね。

ステップ1:ランダムな合言葉(コードベリファイア)を作る

まずは、誰にも破られない長くてランダムな文字列を作ります。

import base64
import hashlib
import os

# 43〜128文字の安全でランダムな文字列(コードベリファイア)を生成します
# 郵便配達の例えにおける「自分だけの合言葉」に相当します
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode("utf-8")

print(f"作成したコードベリファイア: {code_verifier}")

ステップ2:合言葉を変身させる(コードチャレンジ)

次に、作った合言葉をハッシュ化(不可逆的に変換)して、認可サーバーに先に伝えるための「コードチャレンジ」を作ります。ここではWeb標準でよく使われる S256(SHA-256方式)を使います。

# コードベリファイアをSHA-256でハッシュ化し、URLセーフなBase64エンコードを行います
# これが認可サーバーに先回りして教える「コードチャレンジ」になります
hashed = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(hashed).rstrip(b"=").decode("utf-8")

print(f"変換されたコードチャレンジ: {code_challenge}")

ステップ3:認可サーバーへリクエストを送るパラメータの設定

実際に認可サーバーへユーザーを誘導する際の、HTTPリクエストのパラメータ構成は以下のようになります。ここで code_challenge と code_challenge_method=S256 を添えるのがポイントです。

# 認可エンドポイントへリクエストを送る際のパラメータ例
authorization_request_params = {
    "response_type": "code",
    "client_id": "your_mobile_app_client_id",
    "redirect_uri": "https://yourapp.example.com/callback",
    "scope": "read write",
    # --- ここからPKCEのパラメータ ---
    "code_challenge": code_challenge,  # 先ほど作った変身後の合言葉
    "code_challenge_method": "S256",  # 変身のルール(SHA-256を指定)
    # -----------------------------
}

そして、ユーザーが無事にログインして認可コード(引換券)を持ち帰った後、アクセストークンを要求する最終局面(トークンエンドポイント)で、最初に隠し持っていた code_verifier をそのままサーバーに提出します。

# トークンエンドポイントへ送るリクエストボディの例
token_request_body = {
    "grant_type": "authorization_code",
    "client_id": "your_mobile_app_client_id",
    "code": "受け取った認可コード(引換券)",
    "redirect_uri": "https://yourapp.example.com/callback",
    "code_verifier": code_verifier,  # 最初に自分で作った「元の合言葉」をここで初めて明かす!
}

サーバー側は、受け取った code_verifier を自分でハッシュ化し、最初に受け取っていた code_challenge と一致するかを裏側で厳格に検証します。見事に一致すれば、晴れてアクセストークンが発給されるというわけですね。

—

4. おわりに

いかがでしたでしょうか? PKCEと聞くと、アルファベットの羅列でなんだか拒絶反応が出てしまいそうになりますが、中身を紐解いてみると「自分で作った合言葉の秘密を最後に証明する」という、非常にエレガントで理にかなった仕組みであることが分かりますよね。

インフラやセキュリティのプロトコルは、私たちが普段何気なく使っている便利なデジタル世界の裏側で、こうした泥臭くも確実な「身元確認の工夫」を何重にも重ねてくれています。
モバイルアプリやSPAの設計に携わる際は、ぜひこのPKCEの仕組みを思い出して、安全で美しいエンドポイント設計を実装してみてください。

それでは、また次回の深淵でお会いしましょう!

コメント

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