PKCEとは?Go言語OAuth 2.0実装で使う安全な認可コードフロー
PKCEは、OAuth 2.0の認可コードフローをより安全にするための仕組みです。読み方は「ピクシー」とされることが多く、正式には Proof Key for Code Exchange です。特に、モバイルアプリ、デスクトップアプリ、単一ページアプリケーションのように、クライアント秘密値を安全に保持しにくい公開クライアントで重要です。RFC 7636では、認可要求ごとに一意なコード検証値を作り、その変換値であるコードチャレンジを認可サーバーへ送り、トークン交換時にコード検証値を提出して照合する流れが定義されています。
Go言語でOAuth 2.0ログインや外部API連携を実装するとき、PKCEは「任意で付ける追加機能」ではなく、今後の標準的な安全設計として考えるべきです。OAuth 2.1の説明では、認可コードフローを使うすべてのOAuthクライアントにPKCEが必要になること、リダイレクトURIは完全一致で比較すべきこと、暗黙的付与や資源所有者パスワード資格情報付与が仕様から省かれることが示されています。
この記事では、PKCEの仕組みをGo言語実装の視点で詳しく解説します。コード検証値、コードチャレンジ、S256、stateとの違い、OpenID Connectとの関係、Goの golang.org/x/oauth2 パッケージを使った実装、テスト、運用、よくある失敗まで、実務でそのまま使える形で整理します。
1. PKCEとは
PKCEとは、OAuth 2.0の認可コードフローで、認可コードを横取りされても第三者がアクセストークンへ交換できないようにするための仕組みです。クライアントは認可開始時にランダムなコード検証値を作り、その変換値であるコードチャレンジを認可サーバーへ送ります。
その後、認可サーバーから戻ってきた認可コードをトークンへ交換するときに、クライアントは元のコード検証値を提出します。認可サーバーは、最初に受け取ったコードチャレンジと、後から受け取ったコード検証値が対応しているかを確認し、一致しなければトークンを発行しません。RFC 7636では、この照合により、認可コードを盗んだ攻撃者がコード検証値を知らない限りトークン交換できないと説明されています。
1.1 PKCEが解決する問題
OAuth 2.0の認可コードフローでは、認可サーバーからクライアントへ認可コードが戻ります。この認可コードが何らかの形で横取りされると、攻撃者がトークン端点へ送ってアクセストークンを取得しようとする可能性があります。
PKCEは、この認可コードだけではトークン交換できない状態を作ります。認可要求を始めたクライアントだけが知っているコード検証値を後で提出させることで、認可コード横取りへの防御を追加します。
1.2 コード検証値
コード検証値は、クライアントが認可要求ごとに作る高エントロピーなランダム文字列です。RFC 7636では、コード検証値は認可要求とトークン要求を関連付けるための暗号学的ランダム文字列として定義され、長さは43文字以上128文字以下とされています。
Goで実装する場合は、時刻、連番、利用者ID、短い乱数などから作ってはいけません。暗号学的に安全な乱数を使い、認可開始からコールバック完了まで、サーバー側セッションや安全なCookieで保持します。
1.3 コードチャレンジ
コードチャレンジは、コード検証値から作られる値で、認可要求時に認可サーバーへ送ります。S256方式では、コード検証値をSHA-256でハッシュし、base64url形式に変換したものがコードチャレンジになります。RFC 7636では、S256の場合に BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge で検証する流れが示されています。
重要なのは、コード検証値そのものを認可URLへ送らないことです。認可URLにはコードチャレンジを送り、トークン交換時だけコード検証値を提出します。
1.4 S256
S256は、コード検証値をSHA-256で変換する方式です。RFC 7636では、コードチャレンジ方式として plain と S256 が定義されていますが、攻撃者が認可要求も観測できる状況への対策として、code_challenge_method はS256または暗号学的に安全な方式にする必要があると説明されています。
実務では、特別な理由がない限りS256を選びます。Goの golang.org/x/oauth2 には、S256用のコードチャレンジを扱う S256ChallengeOption と、トークン交換時にコード検証値を渡す VerifierOption が用意されています。
1.5 Go言語で使う場面
Go言語では、WebアプリケーションのOAuth 2.0ログイン、外部プロバイダー連携、社内認可基盤との接続、コマンドライン認可、デバイス認可などでPKCEを使います。golang.org/x/oauth2 では GenerateVerifier がPKCEコード検証値を生成し、認可URLとトークン交換の両方に渡す使い方が公式ドキュメントで示されています。
特に新規実装では、機密クライアントか公開クライアントかにかかわらず、PKCEを前提に設計するのが安全です。RFC 9700はOAuth 2.0の最新の安全実装慣行をまとめ、既存仕様の脅威モデルと安全助言を更新・拡張する文書として発行されています。
2. PKCEが必要になる理由
PKCEが必要になる最大の理由は、認可コードが単独で強い秘密情報になってしまう状態を避けるためです。認可コードは短命で一度だけ使う前提ですが、横取りされてすぐにトークン交換されると、アクセストークンが攻撃者へ渡る可能性があります。
PKCEを使うと、認可コードに加えてコード検証値が必要になります。攻撃者が認可コードだけを手に入れても、コード検証値を知らなければトークン交換に失敗します。
2.1 認可コード横取り
認可コード横取りは、リダイレクトURI、OSログ、ブラウザ履歴、悪意あるアプリ、通信経路周辺のログなどから認可コードが漏れる状況で問題になります。RFC 7636では、OSのHTTPログ情報漏えいによって攻撃が観測されたことが説明されています。
GoのサーバーサイドWebアプリでも、認可コードをログへ出したり、エラー画面へ表示したり、不要に長く保存したりすると危険です。PKCEは横取り後の悪用を防ぐ追加防御になります。
2.2 公開クライアント
公開クライアントとは、クライアント秘密値を安全に保持できないクライアントです。モバイルアプリやデスクトップアプリでは、アプリ内に秘密値を埋め込んでも解析される可能性があります。
PKCEはもともと、このような公開クライアントを守るために作られました。クライアント秘密値に頼らず、認可要求ごとに生成される一時的なコード検証値でトークン交換を守ります。
2.3 機密クライアントでも使う理由
機密クライアントは、サーバー側でクライアント秘密値を安全に保持できるクライアントです。従来は、サーバーサイドWebアプリではPKCEを必須にしない実装もありました。
しかし、現在の安全設計では、機密クライアントでもPKCEを入れる価値があります。OAuth 2.1では、認可コードフローを使うすべてのOAuthクライアントにPKCEが必要になる方向が示されています。
2.4 認可サーバー側の検証
PKCEはクライアントだけで完結する仕組みではありません。認可サーバーが、認可要求時に受け取ったコードチャレンジを認可コードと結び付け、トークン交換時に受け取ったコード検証値を照合する必要があります。
認可サーバー側がPKCEを正しく検証しなければ、防御は成立しません。RFC 7636では、トークン端点がコード検証値を変換し、保存済みコードチャレンジと比較して、不一致ならアクセスを拒否する流れが示されています。
2.5 防御の重ね合わせ
PKCEは重要ですが、単独ですべてのOAuth攻撃を防ぐものではありません。state、リダイレクトURI完全一致、TLS、短命トークン、ログ秘匿、スコープ最小化も必要です。
Go実装では、PKCEだけを追加して安心するのではなく、認可コードフロー全体の安全性を確認します。特にstateとPKCEは役割が違うため、どちらか片方だけで済ませる設計は避けます。
3. PKCEの流れ
PKCEの流れは、認可開始、コードチャレンジ送信、認可コード受信、コード検証値送信、認可サーバー照合、トークン発行という順番で進みます。通常の認可コードフローに、コード検証値とコードチャレンジの対応確認が追加される形です。
Goで実装するときは、この流れをハンドラー単位で分けると理解しやすくなります。ログイン開始ハンドラーでコード検証値を作り、コールバックハンドラーで同じ値を取り出してトークン交換へ渡します。
3.1 認可開始
認可開始時、クライアントはコード検証値を生成します。Goの golang.org/x/oauth2 では、GenerateVerifier が32オクテットのランダム性を持つPKCEコード検証値を生成し、認可ごとに新しい値を使うことが説明されています。
このコード検証値は、後でトークン交換に必要になります。したがって、認可URLへリダイレクトする前に、セッションまたは安全なCookieへ保存します。
3.2 コードチャレンジ送信
クライアントは、コード検証値からコードチャレンジを作り、認可URLへ含めます。Goの S256ChallengeOption は、コード検証値からS256方式のコードチャレンジを導出し、AuthCodeURL またはデバイス認可で使うためのオプションとして説明されています。
このとき、stateも同時に送るのが実務上の安全な設計です。PKCEは認可コード横取り対策、stateは認可要求とコールバックの対応確認に役立ちます。
3.3 認可コード受信
利用者が認可サーバーで同意すると、認可サーバーはクライアントのリダイレクトURIへ認可コードを返します。Goアプリケーションは、コールバックハンドラーで code と state を受け取ります。
この段階で、まずstateを検証します。stateが一致しない場合は、認可コードをトークンへ交換せずに拒否します。
3.4 コード検証値送信
stateが正しい場合、Goアプリケーションは保存していたコード検証値を取り出し、認可コードと一緒にトークン交換へ渡します。golang.org/x/oauth2 では、VerifierOption がPKCEコード検証値を返すオプションであり、Config.Exchange に渡すものとして説明されています。
この値が認可開始時のコード検証値と違う場合、認可サーバーはトークン発行を拒否します。ここで初めて、認可要求を始めたクライアントであることの証明が成立します。
3.5 トークン発行
認可サーバーは、受け取ったコード検証値を変換し、保存済みのコードチャレンジと比較します。RFC 7636では、S256の場合、コード検証値をSHA-256でハッシュしてbase64urlエンコードした値をコードチャレンジと比較することが示されています。
一致した場合だけ、アクセストークンやリフレッシュトークンが発行されます。Goアプリケーション側では、取得したトークンをログに出さず、安全な保存先へ格納します。
4. Go言語でのPKCE実装
Go言語でPKCEを実装する場合、現在は golang.org/x/oauth2 のPKCE支援関数を使うのが扱いやすいです。以前のように自分でコード検証値やコードチャレンジを組み立てる必要は少なくなっています。
実装の中心は、GenerateVerifier、S256ChallengeOption、VerifierOption の三つです。認可URL生成時にS256チャレンジを付け、トークン交換時に同じ検証値を渡します。
4.1 Configの準備
まず、OAuth 2.0クライアント設定を作ります。クライアントID、クライアント秘密値、リダイレクトURI、権限範囲、認可URL、トークンURLを定義します。
クライアント秘密値はコードへ直書きしません。本番では環境変数や秘密情報管理サービスから読み込み、ログにも出さないようにします。
設定例
package main
import (
"os"
"golang.org/x/oauth2"
)
func oauthConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: os.Getenv("OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
RedirectURL: os.Getenv("OAUTH_REDIRECT_URL"),
Scopes: []string{
"profile",
"email",
},
Endpoint: oauth2.Endpoint{
AuthURL: os.Getenv("OAUTH_AUTH_URL"),
TokenURL: os.Getenv("OAUTH_TOKEN_URL"),
},
}
}
4.2 コード検証値生成
Goの oauth2.GenerateVerifier() を使うと、PKCE用のコード検証値を生成できます。公式ドキュメントでは、この値は認可ごとに新しく作り、認可URLには S256ChallengeOption、トークン交換には VerifierOption と一緒に使うことが説明されています。
生成したコード検証値は、認可開始からコールバックまで保持する必要があります。サーバー側セッションへ保存するのが安全ですが、小規模な例ではHTTP専用Cookieで保持することもあります。
生成例
verifier := oauth2.GenerateVerifier()
4.3 認可URL生成
認可URL生成時には、stateとPKCEチャレンジを一緒に渡します。S256ChallengeOption(verifier) を使うことで、S256方式のコードチャレンジが認可URLへ追加されます。
stateは別途ランダムに生成し、同じくセッションへ保存します。PKCEとstateは役割が違うため、どちらも使う設計が安全です。
認可URL例
url := config.AuthCodeURL(
state,
oauth2.AccessTypeOffline,
oauth2.S256ChallengeOption(verifier),
)
4.4 トークン交換
コールバックでは、認可コードを受け取った後、保存していたコード検証値を VerifierOption として Exchange に渡します。VerifierOption は Config.Exchange に渡すPKCEコード検証値オプションとして公式ドキュメントに説明されています。
ここで使うコード検証値は、認可開始時に生成したものと同じでなければなりません。違う値を渡すと、認可サーバー側で照合に失敗します。
交換例
token, err := config.Exchange(
r.Context(),
code,
oauth2.VerifierOption(verifier),
)
if err != nil {
http.Error(w, "token exchange failed", http.StatusBadGateway)
return
}
_ = token
4.5 完整なハンドラー例
以下は、GoのWebアプリでPKCEを使う最小構成の例です。実務では、Cookie保存ではなくサーバー側セッション、暗号化、失敗ログ、OpenID ConnectのIDトークン検証を追加します。
Go実装例
package main
import (
"crypto/rand"
"encoding/base64"
"net/http"
"golang.org/x/oauth2"
)
var config = oauthConfig()
func randomState() string {
b := make([]byte, 32)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
state := randomState()
verifier := oauth2.GenerateVerifier()
http.SetCookie(w, &http.Cookie{
Name: "oauth_state",
Value: state,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{
Name: "pkce_verifier",
Value: verifier,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
url := config.AuthCodeURL(
state,
oauth2.S256ChallengeOption(verifier),
)
http.Redirect(w, r, url, http.StatusFound)
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
stateCookie, err := r.Cookie("oauth_state")
if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
http.Error(w, "invalid state", http.StatusBadRequest)
return
}
verifierCookie, err := r.Cookie("pkce_verifier")
if err != nil || verifierCookie.Value == "" {
http.Error(w, "missing pkce verifier", http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "missing code", http.StatusBadRequest)
return
}
token, err := config.Exchange(
r.Context(),
code,
oauth2.VerifierOption(verifierCookie.Value),
)
if err != nil {
http.Error(w, "token exchange failed", http.StatusBadGateway)
return
}
_ = token
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("login completed with PKCE"))
}
5. コード検証値
コード検証値はPKCEの中核です。これが弱い、再利用される、漏えいする、保存に失敗する、認可要求とトークン交換で一致しないと、PKCEの防御効果は失われます。
Go実装では、コード検証値を「一時的な秘密値」として扱います。アクセストークンほど長期的ではありませんが、認可コード交換の成否を決める重要な値です。
5.1 長さ
RFC 7636では、コード検証値の長さは43文字以上128文字以下と定義されています。使える文字は英字、数字、ハイフン、ピリオド、アンダースコア、チルダなどの非予約文字です。
Goで自前生成する場合は、この条件を満たす必要があります。ただし、oauth2.GenerateVerifier() を使えば、このような細かい仕様を直接扱わずに済みます。
5.2 ランダム性
コード検証値は暗号学的に安全なランダム値である必要があります。推測可能な値では、攻撃者がコード検証値を当ててトークン交換できる可能性があります。
Goで自前生成するなら crypto/rand を使います。math/rand、時刻、利用者ID、セッションIDの単純な加工は使わない方が安全です。
5.3 使い捨て
コード検証値は認可要求ごとに新しく作ります。同じコード検証値を複数の認可要求で使い回すと、漏えい時の影響が広がります。
golang.org/x/oauth2 の公式ドキュメントでも、新しい検証値を各認可ごとに生成するべきと説明されています。
5.4 保存期間
コード検証値は、認可開始からコールバック完了までの短い期間だけ必要です。トークン交換が終わったら削除します。
長く保存する必要はありません。GoのWebアプリでは、コールバック成功後にCookieを期限切れにするか、サーバー側セッションから削除します。
5.5 自前生成例
golang.org/x/oauth2 の関数を使えない環境では、以下のように生成できます。ただし、通常は公式パッケージの GenerateVerifier() を優先します。
自前生成例
package pkce
import (
"crypto/rand"
"encoding/base64"
)
func GenerateVerifier() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
6. コードチャレンジ
コードチャレンジは、認可サーバーへ先に送る検証用の値です。コード検証値そのものを送るのではなく、S256方式ではハッシュ化された値を送ります。
この設計により、認可URLや認可要求の周辺ログからコードチャレンジが見えても、元のコード検証値を復元しにくくなります。トークン交換時には、元のコード検証値を提出して照合します。
6.1 S256方式
S256方式では、コード検証値をASCII文字列としてSHA-256でハッシュし、その結果をbase64urlエンコードします。RFC 7636では、この値が認可要求時のコードチャレンジと一致するかを確認する式が示されています。
Goでは、公式パッケージの S256ChallengeOption を使うのが簡単です。自前で作る場合でも、標準ライブラリの crypto/sha256 と encoding/base64 で実装できます。
6.2 plain方式
plain方式は、コード検証値をそのままコードチャレンジとして送る方式です。互換性目的で仕様に残っていますが、実務ではS256を使うべきです。
認可要求のログを攻撃者が見られる状況では、plain方式だとコード検証値そのものが漏れる可能性があります。RFC 7636でも、より高度な攻撃状況への対策としてS256などの暗号学的に安全な方式が必要と説明されています。
6.3 認可URLのパラメータ
PKCEでは、認可URLに code_challenge と code_challenge_method を含めます。RFC 7636では、code_challenge は必須、code_challenge_method は省略時にplain扱いになると説明されています。
実務では、省略せずに S256 を明示します。Goの S256ChallengeOption を使えば、S256用のパラメータを適切に付けられます。
6.4 認可サーバーの保存
認可サーバーは、受け取ったコードチャレンジと方式を、発行する認可コードに関連付けて保存します。後でトークン交換が来たとき、この関連付けを使って検証します。
クライアント側だけがPKCEを送っても、認可サーバーが保存・検証しなければ意味がありません。認可サーバーを自社運用する場合は、この保存と照合を必ず実装します。
6.5 自前チャレンジ生成例
以下はS256コードチャレンジを自前で作る例です。Goの公式OAuth 2.0パッケージを使える場合は、通常 S256ChallengeOption を使う方が安全で簡潔です。
S256生成例
package pkce
import (
"crypto/sha256"
"encoding/base64"
)
func S256Challenge(verifier string) string {
sum := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(sum[:])
}
7. stateとの違い
PKCEとstateは、どちらもOAuth 2.0認可コードフローの安全性に関係しますが、役割が違います。PKCEは認可コードの横取り対策であり、stateは認可要求とコールバックを結び付けるために使います。
Go実装では、どちらか一方だけを入れるのではなく、両方を入れるのが安全です。PKCEがあるからstateはいらない、stateがあるからPKCEはいらない、という考え方は危険です。
7.1 stateの役割
stateは、クライアントが認可開始時に作り、認可URLへ含めるランダム値です。コールバック時に同じstateが戻ってきたかを確認することで、認可要求と応答の対応を検証します。
Goアプリケーションでは、stateをセッションへ保存し、戻ってきた値と比較します。一致しない場合は、認可コードを交換せずに拒否します。
7.2 PKCEの役割
PKCEは、認可コードをトークンへ交換するクライアントが、認可要求を始めたクライアントであることを証明するために使います。コード検証値を知らない攻撃者は、認可コードを盗んでもトークン交換できません。
RFC 7636では、認可コードを取得した後、クライアントがトークン端点へコード検証値を送り、認可サーバーがコードチャレンジと照合する流れが定義されています。
7.3 攻撃対象の違い
stateは、主に認可応答の差し替えやセッション混同に対する対策です。攻撃者が自分の認可結果を別の利用者のセッションに結び付けるような問題を防ぎます。
PKCEは、認可コードそのものを横取りされても悪用されにくくする対策です。守る対象が違うため、両方を使う必要があります。
7.4 比較表
以下は、stateとPKCEの違いです。実装判断ではなく、役割整理のための表です。
| 項目 | state | PKCE |
|---|---|---|
| 主な目的 | 認可要求と応答の対応確認 | 認可コード横取り対策 |
| 生成タイミング | 認可開始時 | 認可開始時 |
| 認可URLへ送る値 | stateそのもの | コードチャレンジ |
| トークン交換時に使う値 | 通常は使わない | コード検証値 |
| Go実装での保存 | セッションまたは安全なCookie | セッションまたは安全なCookie |
| 省略時の危険 | CSRFやセッション混同 | 認可コード横取り後の悪用 |
7.5 併用設計
GoのWebアプリでは、ログイン開始時にstateとコード検証値を同時に生成します。stateはコールバック時に照合し、コード検証値はトークン交換へ渡します。
コールバック処理では、まずstateを確認し、次にコード検証値を取得し、最後に認可コードを交換します。この順番にすると、不正なコールバックを早い段階で拒否できます。
8. OAuth 2.1との関係
OAuth 2.1は、OAuth 2.0の実装経験と安全推奨を取り込み、より安全な方向へ整理する流れです。PKCEはその中で重要な位置を占めています。
OAuth.netのOAuth 2.1説明では、認可コードフローを使うすべてのOAuthクライアントでPKCEが必要になること、リダイレクトURIの完全一致、暗黙的付与の省略などが示されています。
8.1 PKCE必須化の流れ
OAuth 2.0時代には、PKCEは主に公開クライアント向けの拡張として扱われていました。しかし現在は、認可コードフロー全体の安全性を高める一般的な対策として見られています。
Goで新規実装する場合、プロバイダーが必須としていなくてもPKCEを入れるのがよい設計です。後から必須化されても、アプリケーション側の変更を小さくできます。
8.2 暗黙的付与の扱い
OAuth 2.1の説明では、暗黙的付与が仕様から省かれることが示されています。暗黙的付与はブラウザへ直接アクセストークンを返すため、現代の安全設計では避けられる傾向があります。
単一ページアプリケーションでも、認可コードフローとPKCEを使う設計が一般的になっています。Goのバックエンドがある場合は、トークンをサーバー側で扱う構成も検討できます。
8.3 リダイレクトURI完全一致
OAuth 2.1の説明では、リダイレクトURIを完全一致で比較することも重要な変更点として示されています。
PKCEがあっても、リダイレクトURIの検証が甘いと危険です。Goアプリケーションでは、環境ごとのリダイレクトURIを固定し、任意の戻り先をクエリで受け取って認可URLへ入れないようにします。
8.4 公開クライアントの更新トークン
OAuth 2.1の説明では、公開クライアントのリフレッシュトークンは送信者制約付き、または一回限りの利用であるべきとされています。
PKCEは認可コード交換を守る仕組みであり、リフレッシュトークンの長期保護とは別問題です。Goアプリケーションでリフレッシュトークンを扱う場合は、保存、ローテーション、失効まで設計します。
8.5 新規実装の方針
新しくGoでOAuth 2.0連携を作るなら、OAuth 2.1の方向に合わせて設計するのが現実的です。認可コードフロー、PKCE、state、完全一致リダイレクトURI、短命アクセストークン、安全なリフレッシュトークン管理を前提にします。
古いサンプルコードではPKCEが省略されていることがあります。実装時には、現在の golang.org/x/oauth2 のPKCE対応関数を使う方が安全で読みやすくなります。
9. WebアプリでのPKCE
GoのサーバーサイドWebアプリでも、PKCEを使う価値があります。サーバー側でクライアント秘密値を守れるとしても、認可コード横取り対策としてPKCEを追加すると防御が厚くなります。
実装では、ログイン開始時にコード検証値を作り、コールバック時に同じ値を取り出してトークン交換に渡します。保存場所と削除タイミングを間違えないことが重要です。
9.1 サーバー側セッション
最も扱いやすいのは、コード検証値をサーバー側セッションへ保存する方法です。ブラウザにはセッションIDだけを持たせ、実際のコード検証値はサーバー側に置きます。
この方法では、Cookie漏えい時の影響を抑えやすくなります。ただし、セッションストアの可用性、期限、削除処理を設計する必要があります。
9.2 Cookie保存
簡単な実装では、HTTP専用Cookieにコード検証値を入れることもあります。この場合は、Secure、HttpOnly、SameSiteを必ず設定します。
ただし、コード検証値は一時的な秘密値です。コールバック完了後は削除し、長くブラウザに残さないようにします。
9.3 複数タブ問題
利用者が複数のタブで同時にログイン開始すると、stateやコード検証値が上書きされる場合があります。その結果、先に開いたタブのコールバックで照合に失敗することがあります。
Go実装では、stateをキーにしてコード検証値を保存すると、この問題に対応しやすくなります。単一のCookie名に一つだけ保存する実装は簡単ですが、複数タブに弱くなります。
9.4 ログアウト
PKCEそのものはログイン開始からトークン交換までの仕組みですが、Webアプリではログアウト時のセッション削除も重要です。認可フロー用のstateやコード検証値が残っていれば削除します。
ログアウトは、アプリケーションセッション、OAuthトークン、認可フロー一時値を分けて扱います。それぞれ保存目的と削除タイミングが違います。
9.5 セッションストア例
以下は、stateをキーにしてコード検証値を保存するための簡単なインターフェース例です。実務ではRedis、データベース、暗号化Cookieなどを使って実装します。
ストア例
package authflow
import "time"
type PKCEStore interface {
Save(state string, verifier string, expiresAt time.Time) error
Find(state string) (string, error)
Delete(state string) error
}
10. 単一ページアプリケーションとPKCE
単一ページアプリケーションでは、クライアント秘密値を安全に保持できません。そのため、認可コードフローとPKCEを組み合わせる設計が重要です。
Goのバックエンドがある場合は、単一ページアプリケーションだけでトークンを直接保持するのではなく、バックエンドを仲介する構成も検討できます。トークン保存場所が安全性を大きく左右します。
10.1 ブラウザ内の危険
ブラウザ内のJavaScriptは、保存領域や実行環境の制約を受けます。アクセストークンやリフレッシュトークンを長期保存すると、クロスサイトスクリプティングなどで漏れる可能性があります。
PKCEは認可コード交換を守りますが、取得後のトークン保存を自動的に守るわけではありません。トークンの保存と更新は別途設計する必要があります。
10.2 バックエンド仲介
Goバックエンドを使う場合、認可コード交換をバックエンド側で行い、ブラウザにはアプリケーションセッションだけを返す構成が取れます。これにより、アクセストークンやリフレッシュトークンをブラウザから遠ざけられます。
この構成では、GoバックエンドがOAuthクライアントとして振る舞います。PKCE、state、セッション、Cookie属性を正しく設定することが重要です。
10.3 CORSとCookie
単一ページアプリケーションとGoバックエンドが別ドメインの場合、CORSとCookie設定が重要になります。SameSite、Secure、許可するOriginを適切に設定します。
認可フローのコールバックURLとアプリケーションの戻り先を混同しないようにします。認可サーバーへ登録するリダイレクトURIは厳密に固定します。
10.4 リフレッシュトークン
公開クライアントでリフレッシュトークンを扱う場合、漏えい時の影響が大きくなります。OAuth 2.1の説明では、公開クライアントのリフレッシュトークンは送信者制約付き、または一回限りの利用であるべきとされています。
Goバックエンドがあるなら、リフレッシュトークンをバックエンド側で保存する構成が扱いやすいです。ブラウザには短命セッションだけを持たせます。
10.5 構成例
以下は、単一ページアプリケーションとGoバックエンドを組み合わせる構成の考え方です。認可コード交換はGoバックエンドで行い、ブラウザにはアプリケーションセッションを返します。
構成例
Browser SPA
-> GET /login
-> Go Backend creates state and PKCE verifier
-> Redirect to Authorization Server
-> Authorization Server redirects to /oauth/callback
-> Go Backend exchanges code with verifier
-> Go Backend stores tokens server-side
-> Go Backend issues secure application session cookie
11. ネイティブアプリとPKCE
ネイティブアプリは、PKCEが特に重要な領域です。アプリに埋め込まれた秘密値は解析される可能性があり、すべてのインストールで同じ秘密値を共有する設計は安全ではありません。
PKCEは、クライアント秘密値に頼らず、認可要求ごとに作る一時的なコード検証値で認可コード交換を守ります。RFC 7636は、公開クライアントがクライアント秘密値を機密として保持できない問題を前提にしています。
11.1 アプリ内ブラウザを避ける
ネイティブアプリでは、システムブラウザを使って認可サーバーへ遷移する設計が一般的です。アプリ内に利用者のパスワード入力画面を作ると、認可サーバーとの信頼境界が崩れます。
GoでデスクトップアプリやCLIを作る場合も、認可サーバーの画面はブラウザで開き、ローカルコールバックやデバイス認可を使う設計が扱いやすいです。
11.2 カスタムスキーム
ネイティブアプリでは、カスタムURIスキームで認可結果を受け取る方式があります。ただし、同じスキームを別アプリが登録できる環境では、認可コード横取りの危険があります。
PKCEはこのような状況で特に役立ちます。認可コードだけを横取りされても、コード検証値を知らなければトークン交換できません。
11.3 ループバック
デスクトップアプリでは、ローカルループバックアドレスで一時的なHTTPサーバーを立て、認可コードを受け取る方法があります。Goでは標準の net/http で実装しやすいです。
この方式でもPKCEを使います。ローカル環境であっても、認可コードが他プロセスやログに露出する可能性を考える必要があります。
11.4 秘密値を埋め込まない
ネイティブアプリにクライアント秘密値を埋め込んでも、攻撃者がアプリを解析して取り出せる可能性があります。そのため、公開クライアントとして扱うのが安全です。
PKCEは、秘密値を持てないクライアントのための防御として設計されています。Go製CLIでも、クライアント秘密値に頼る実装は避けます。
11.5 CLI例
以下は、GoのCLIでPKCE検証値を生成し、認可URLを表示する簡略例です。実務では、ローカルHTTPサーバーでコールバックを受けるか、デバイス認可を使います。
CLI例
package main
import (
"fmt"
"golang.org/x/oauth2"
)
func main() {
conf := oauthConfig()
state := "replace-with-random-state"
verifier := oauth2.GenerateVerifier()
url := conf.AuthCodeURL(
state,
oauth2.S256ChallengeOption(verifier),
)
fmt.Println("Open this URL:")
fmt.Println(url)
// コールバックで受け取った認可コードを交換するとき、
// 同じ verifier を oauth2.VerifierOption(verifier) として渡す。
}
12. 認可サーバー側の実装
自社で認可サーバーを作る場合、PKCEの検証を正しく実装しなければなりません。クライアントがコードチャレンジを送っても、認可サーバーが検証しなければ防御は成立しません。
多くの場合、既存の認可サーバー製品やID基盤を使う方が安全です。どうしても自前で作る場合は、RFC 7636とRFC 9700の安全慣行を確認しながら設計します。
12.1 認可要求の受信
認可サーバーは、認可要求に含まれるコードチャレンジとコードチャレンジ方式を受け取ります。PKCEを必須にする場合、コードチャレンジがない要求は拒否します。
RFC 7636では、PKCEを要求する公開クライアントがコードチャレンジを送らない場合、認可端点は invalid_request を返すべきと説明されています。
12.2 認可コードとの関連付け
認可サーバーは、発行する認可コードにコードチャレンジと方式を関連付けて保存します。保存期間は短くし、認可コードは一回だけ使えるようにします。
保存先には、認可コード、クライアントID、リダイレクトURI、コードチャレンジ、方式、期限、利用済み状態を含めます。これらがないと、安全な照合ができません。
12.3 トークン端点の検証
トークン端点では、認可コード、クライアント情報、リダイレクトURI、コード検証値を確認します。コード検証値を方式に従って変換し、保存済みのコードチャレンジと比較します。
RFC 7636では、トークン端点がコード検証値を変換して、以前関連付けられたコードチャレンジと比較する流れが明確に示されています。
12.4 plainの扱い
後方互換性のためにplain方式を受け付けるかどうかは、認可サーバーの方針によります。新規環境ではS256だけを許可する方が安全です。
plainを許可すると、認可要求を観測できる攻撃者に対して弱くなる可能性があります。実務では、S256以外を拒否する設定を推奨します。
12.5 検証例
以下は、認可サーバー側でS256を検証する簡略例です。実務では、一定時間比較、認可コードの一回利用、期限、クライアントID、リダイレクトURIも確認します。
検証例
package serverpkce
import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
)
func VerifyS256(verifier string, expectedChallenge string) bool {
sum := sha256.Sum256([]byte(verifier))
actual := base64.RawURLEncoding.EncodeToString(sum[:])
return subtle.ConstantTimeCompare(
[]byte(actual),
[]byte(expectedChallenge),
) == 1
}
13. テスト
PKCE実装では、正常系だけでなく失敗系のテストが重要です。コード検証値がない、stateが違う、コード検証値が違う、S256方式が間違っている、再利用された認可コードを拒否する、といったケースを確認します。
Goでは、httptest、インメモリストア、モック認可サーバーを使って、PKCEの流れを再現できます。認可サーバーを本物に接続する結合テストと、ローカルで完結する単体テストを分けると保守しやすくなります。
13.1 コード検証値生成テスト
コード検証値生成では、空でないこと、毎回異なること、長さが条件を満たすことを確認します。oauth2.GenerateVerifier() を使う場合でも、アプリ側の保存処理をテストします。
生成値そのものを固定値として期待するテストは避けます。ランダム値であるため、形式と保存・取得の流れを確認します。
13.2 コードチャレンジテスト
S256変換を自前実装している場合は、既知の入力と期待値でテストします。公式パッケージを使う場合でも、認可URLにPKCEパラメータが入ることを確認できます。
S256ChallengeOption を使うと実装ミスは減りますが、認可URL生成時にそのオプションを渡し忘れる問題はアプリ側のテストで検出すべきです。
13.3 コールバック失敗テスト
コールバックでは、state不一致、コードなし、コード検証値なし、トークン交換失敗をテストします。これらの失敗を安全に拒否できることが重要です。
失敗時に認可コードやトークンをログへ出していないかも確認します。OAuth実装のテストでは、セキュリティ上の副作用も見るべきです。
13.4 認可サーバー検証テスト
認可サーバーを自前実装する場合、コード検証値が間違っているとトークンを発行しないことを確認します。認可コードの再利用も拒否します。
PKCEは、認可サーバー側の保存と照合が正しくて初めて成立します。クライアント側だけをテストしても不十分です。
13.5 Goテスト例
以下は、S256変換の自前実装をテストする例です。公式パッケージを使う場合でも、考え方の確認に役立ちます。
テスト例
package pkce_test
import (
"testing"
"example.com/app/pkce"
)
func TestS256ChallengeIsStable(t *testing.T) {
verifier := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
challenge1 := pkce.S256Challenge(verifier)
challenge2 := pkce.S256Challenge(verifier)
if challenge1 == "" {
t.Fatal("challenge must not be empty")
}
if challenge1 != challenge2 {
t.Fatal("same verifier must produce same challenge")
}
}
func TestS256ChallengeDiffers(t *testing.T) {
a := pkce.S256Challenge("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
b := pkce.S256Challenge("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
if a == b {
t.Fatal("different verifier should produce different challenge")
}
}
14. よくある失敗
PKCEは仕組み自体は短いですが、実装では失敗しやすい点があります。特に、コード検証値の保存、S256指定、stateとの混同、ログ出力、トークン交換時の渡し忘れが多い問題です。
Go実装では、ハンドラーが分かれるため、ログイン開始時に作った値をコールバックで正しく取り出せる設計が重要です。値の寿命と保存場所を明確にします。
14.1 コード検証値を保存しない
認可URL生成時にコード検証値を作っても、保存していなければコールバックでトークン交換に使えません。結果として、交換時にPKCE検証が失敗します。
Goでは、stateとコード検証値をペアにして保存するのが安全です。単にグローバル変数に置くと、複数利用者や複数リクエストで壊れます。
14.2 plainを使う
plain方式を使うと、コード検証値そのものを認可要求側へ露出させることになります。RFC 7636では、攻撃者が認可要求を観測できる場合への対策としてS256などの暗号学的に安全な方式が必要とされています。
Goの S256ChallengeOption を使えば、S256方式を簡単に指定できます。新規実装ではplainを選ばない方が安全です。
14.3 stateとPKCEを混同する
stateはコールバックの対応確認、PKCEは認可コード横取り対策です。両者は似たタイミングで生成されますが、用途は違います。
stateをコード検証値として使ったり、コード検証値をstate代わりに使ったりする設計は避けます。値を分け、保存名も分けると保守しやすくなります。
14.4 認可コードをログに出す
認可コードやコード検証値をログに出すと、攻撃者がログから値を取得する可能性があります。PKCEは認可コードだけの漏えいには強くなりますが、コード検証値まで漏れれば防御は崩れます。
Goのデバッグログでは、URL全体を出すことがあります。認可コールバックのクエリ文字列をそのまま記録しないように注意します。
14.5 リダイレクトURI検証を甘くする
PKCEを入れても、リダイレクトURIの検証が甘いと危険です。OAuth 2.1の説明では、リダイレクトURIの完全一致比較が重要な項目として示されています。
Goアプリケーションでは、リダイレクトURIを設定値として固定します。利用者入力や任意のクエリ値から認可サーバーへ渡すリダイレクトURIを組み立てないようにします。
15. 運用しやすいPKCE設計
PKCEを実務で安定運用するには、コードだけでなく、設定、ログ、監視、テスト、プロバイダー差分、移行計画まで含めて設計します。ログインは利用者体験に直結するため、失敗時に原因を追えることも重要です。
Goアプリケーションでは、PKCE処理を認可フロー用パッケージとして切り出すと保守しやすくなります。ログイン開始、コールバック、セッション、トークン保存を分けて設計します。
15.1 設定管理
認可URL、トークンURL、クライアントID、リダイレクトURI、スコープは環境ごとに変わります。Goコードへ直接書かず、設定として管理します。
ただし、リダイレクトURIは自由入力にしないことが重要です。認可サーバー側に登録したURIと完全一致する値を使います。
15.2 ログ設計
PKCE関連ログでは、成功、失敗、プロバイダー、理由、要求IDを記録します。しかし、認可コード、コード検証値、アクセストークン、リフレッシュトークンは出してはいけません。
障害調査に必要な情報と、漏えいしてはいけない情報を分けます。Goの構造化ログを使う場合も、値のマスキング規則を用意します。
15.3 監視
ログイン開始数、コールバック成功数、state不一致数、PKCE検証失敗数、トークン交換失敗数を監視します。突然失敗が増えた場合、認可サーバー設定、リダイレクトURI、Cookie属性、ブラウザ制限が原因かもしれません。
PKCE検証失敗が多い場合、複数タブ問題、Cookie保存失敗、セッション期限切れ、コード検証値の渡し忘れを疑います。
15.4 移行
既存のOAuth 2.0実装にPKCEを追加する場合、認可サーバーがPKCEをサポートしているかを確認します。サポートしているなら、まず検証環境で認可URLとトークン交換の両方を確認します。
一部の古いプロバイダーでは、PKCE必須設定やS256対応に差があります。OAuth 2.1の方向に合わせるなら、S256対応プロバイダーを前提にするのが安全です。
15.5 最終構成例
以下は、Go WebアプリでPKCEを運用するための構成例です。認可フロー用の一時値、アプリケーションセッション、トークン保存を分けると、責務が明確になります。
ディレクトリ構成例
.
├── cmd
│ └── web
│ └── main.go
├── internal
│ ├── oauthflow
│ │ ├── config.go
│ │ ├── login.go
│ │ ├── callback.go
│ │ ├── pkce_store.go
│ │ └── state.go
│ ├── session
│ │ └── store.go
│ ├── tokenstore
│ │ └── encrypted_store.go
│ └── user
│ └── repository.go
└── go.mod
ルーティング例
package main
import (
"net/http"
"example.com/app/internal/oauthflow"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/login", oauthflow.LoginHandler)
mux.HandleFunc("/oauth/callback", oauthflow.CallbackHandler)
_ = http.ListenAndServe(":8080", mux)
}
おわりに
PKCEは、OAuth 2.0の認可コードフローを安全にするための重要な仕組みです。認可要求ごとにコード検証値を作り、その変換値であるコードチャレンジを認可サーバーへ送り、トークン交換時に元のコード検証値を提出して照合します。この仕組みにより、認可コードを横取りされても、コード検証値を知らない攻撃者はアクセストークンを取得できません。
Go言語で実装する場合は、golang.org/x/oauth2 の GenerateVerifier、S256ChallengeOption、VerifierOption を使うと、PKCEを簡潔に組み込めます。公式ドキュメントでも、認可ごとに新しい検証値を生成し、認可URLにはS256チャレンジ、トークン交換には検証値を渡す使い方が示されています。
ただし、PKCEだけでOAuth 2.0実装全体が安全になるわけではありません。state、リダイレクトURI完全一致、TLS、短命トークン、秘密値管理、ログ秘匿、セッション保護、テスト、監視を合わせて設計することが重要です。新規のGoアプリケーションでは、OAuth 2.1の方向に合わせ、認可コードフローとPKCEを最初から標準構成として扱うのが安全で保守しやすい実装になります。
EN
JP
KR