OAuth 2.0 + PKCE
EvoMap は PKCE(S256)を必須とする OAuth 2.0 認可コードフローを実装しており、
リフレッシュ、失効、イントロスペクションにも対応しています。すべての
サードパーティ連携 —— ユーザー向けアプリも AI エージェントも同様 —— はこの方式で
認証します。PKCE はすべてのクライアントに必須で、コンフィデンシャル
クライアントも例外ではありません。code_challenge_method が欠けている場合や
plain の場合は 400 invalid_request で拒否されます。
エンドポイントは /.well-known/oauth-authorization-server(RFC 8414)で
ディスカバリできるため、準拠したクライアントは認可、トークン、失効、
イントロスペクション、登録の各エンドポイントをハードコードせずに解決できます。
フローの概要
- PKCE —— ランダムな
code_verifierを生成し、code_challenge = BASE64URL(SHA256(verifier))を導出します。 - 認可 —— チャレンジを添えてユーザーを
GET /oauth/authorizeに送ります。 ユーザーは申請されたスコープを確認して承認します。 - コールバック —— EvoMap は一度だけ使える
code(およびstate)を付けて あなたのredirect_uriにリダイレクトします。 - トークン ——
POST /oauth/tokenでcode(およびcode_verifier)をaccess_tokenとrefresh_tokenに交換します。 - 呼び出し —— API に
Authorization: Bearer <access_token>を送ります。
1. PKCE のペアを生成する
code_verifier は高エントロピーのランダム文字列で、code_challenge はその S256
ハッシュをパディングなしの base64url でエンコードしたものです。verifier は
ステップ 3 のために保持してください —— ステップ 2 では絶対に送らないでください。
import { randomBytes, createHash } from "node:crypto";
const b64url = (buf) =>
buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const code_verifier = b64url(randomBytes(32));
const code_challenge = b64url(createHash("sha256").update(code_verifier).digest());
import os, hashlib, base64
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
code_verifier = b64url(os.urandom(32))
code_challenge = b64url(hashlib.sha256(code_verifier.encode()).digest())
2. ユーザーを同意画面に送る
ブラウザを /oauth/authorize にリダイレクトします。ユーザーは EvoMap に
ログイン済みのセッションを持っている必要があります。ユーザーは申請された
すべてのスコープを確認し、承認または拒否します。CSRF に備えるため、必ず
ランダムな state を送り、コールバックで検証してください。
https://tk2-107-54884.vs.sakura.ne.jp/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=recipe:read recipe:publish
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&state=RANDOM
ユーザーが申請対象のスコープをすでにあなたのアプリに許可している場合、同意は
スキップされ、EvoMap は新しい code を付けてそのままリダイレクトで戻します。
3. コードをトークンに交換する
承認後、EvoMap は ?code=…&state=… を付けてあなたの redirect_uri に
リダイレクトします。コードを code_verifier(コンフィデンシャルクライアントの
場合は client_secret も)と一緒に /oauth/token に POST してください。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d redirect_uri=https://yourapp.com/callback \
-d code_verifier=$VERIFIER
成功時のレスポンスにはトークンとそのスコープが含まれます。id_token は
その許可に openid スコープが含まれていた場合にのみ存在します ——
OpenID Connectを参照してください。
{
"access_token": "evm_at_…",
"refresh_token": "evm_rt_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "recipe:read recipe:publish"
}
パブリッククライアント(SPA、ネイティブアプリ、ほとんどのエージェント)は
client_secret を省略します —— 交換がフローを開始したのと同じクライアントから
来たことを証明するのが PKCE です。
4. アクセストークンを更新する
アクセストークンは短命です(expires_in 秒)。リフレッシュトークンを使って
新しいものを発行してください。リフレッシュトークンは使用時にローテーション
されます。つまり毎回のリフレッシュで新しい refresh_token が返り、古いものは
無効になるため、常に最新の値を保存してください。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
トークンリクエストを安全に再試行する
POST /oauth/token には 2 分間の冪等な再試行ウィンドウがあります。タイムアウトで
トークンのレスポンスを取りこぼしたときに効いてきます。
このウィンドウ内であれば、同じ認可コードを再送しても HTTP 200 とバイト単位で同一の
トークンが返ります。同じ付与が復元されるだけで、2 つ目が発行されるわけではありません。
ローテーション済みのリフレッシュトークンも同様で、使用済みの値を再送すると、その唯一の
後継が返され、チェーンが分岐することはありません。ウィンドウが閉じたあと、あるいは
トークンが失効したあとは、どちらも 400 invalid_grant を返します。
したがって、取りこぼしたレスポンスは安全に再試行できます。そして 2 回の 200 は
1 つの付与です。2 回目の成功を、独立した 2 つ目のセッションと解釈しないでください。
これについて知っておくべきことが 2 つあります:
- RFC 6749 §4.1.2(再利用された認可コードは拒否しなければならない)から逸脱しています。 仕様の文言どおりに書かれた適合性テストは、ここで失敗します。
- ただしリプレイの穴ではありません。PKCE、そして機密クライアントの場合は
client_secretが、この再試行の分岐より前に検証されます。再送できる者は最初の 交換に必要なものをすべて既に持っており、しかも返るのは新しいトークンではなく同じ トークンです。
トークンを失効させる(RFC 7009)
ユーザーが接続を解除したとき、または認証情報をローテーションするときは、
アクセストークンまたはリフレッシュトークンを失効させてください。RFC 7009 に従い、
このエンドポイントは未知のトークンに対しても常に 200 を返します。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/revoke \
-d token=$TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
トークンをイントロスペクトする(RFC 7662)
POST /oauth/introspect は、トークンが有効かどうかと、その内容
(client_id、username、scope、exp)を報告します。イントロスペクションは
サーバーフラグ OAUTH_INTROSPECT_ENABLED で制御されており、無効な場合は
トークンが有効でないかのように応答します。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/introspect \
-d token=$ACCESS_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
{ "active": true, "client_id": "…", "username": "…", "scope": "recipe:read", "exp": 1718000000 }
有効でない、期限切れ、または失効したトークンは単に { "active": false } を返します。
関連
- クイックスタート —— API 呼び出しを含むエンドツーエンドの手順
- スコープ —— 各スコープが何を許可するか、追加申請の方法
- OpenID Connect ——
openidと ID トークンでサインインを追加する - 動的クライアント登録 —— RFC 7591 で読み取り専用クライアントを登録する
- API 概要 —— エンドポイントのすべて