顧客サイトから社内システムのAPIを呼ぶためのAPIキーを、管理画面から発行できるようにしたときの実装メモです。
要件: 1顧客につき有効なキーは常に1つだけ
SanctumのcreateToken()は呼ぶたびに新しいトークンを発行でき、1つのモデルに複数のトークンを持たせることもできます。しかし、顧客向けAPIキーでは「有効なキーは常に1つ」というシンプルなルールにしました。複数の古いキーが有効なまま残っていると、どれが今使われているキーか分からなくなり、失効させたいときに全部消せているか不安になるためです。
// app/Http/Controllers/Admin/CustomerApiTokenController.php
public function store(Customer $customer): RedirectResponse
{
$this->authorize('update', $customer);
$customer->tokens()->delete();
$token = $customer->createToken('site-api')->plainTextToken;
return redirect()
->route('admin.customers.show', $customer)
->with('status', 'APIキーを発行しました(このページを離れると二度と表示されません)。')
->with('plainTextApiToken', $token);
}
新しいキーを発行する前に、必ず$customer->tokens()->delete()で既存のトークンを全部消してからcreateToken()します。これにより「発行ボタンを押す=古いキーを失効させて新しいキーに切り替える」という、運用者にとって分かりやすい1つの操作になります。
平文トークンは1回しか手に入らない
Sanctumの仕様上、plainTextToken(実際にAPIリクエストのヘッダーに使う生の文字列)は、発行した瞬間のレスポンスでしか取得できません。DBには元の文字列のハッシュしか保存されないので、あとから「もう一度見せて」はできません。
このため、redirect()->with('plainTextApiToken', $token)でリダイレクト先の画面に1回だけ流し、ビュー側で「今回発行したキーです。この画面を離れると二度と表示されません」という警告と共に表示します。セッションのフラッシュデータは次のリクエストで消えるので、リロードすれば自然に見えなくなります。
失効も「削除するだけ」
public function destroy(Customer $customer): RedirectResponse
{
$this->authorize('update', $customer);
$customer->tokens()->delete();
return redirect()
->route('admin.customers.show', $customer)
->with('status', 'APIキーを失効させました。');
}
発行と失効、どちらも$customer->tokens()->delete()を使っている点が対称的です。「今あるキーを全部消す」という同じ操作に、その後createToken()するかしないかの違いしかありません。実装がシンプルだと、レビューする側も「このコードが何をするか」を一目で把握できます。
authorize('update', $customer)で権限を1箇所に集約
APIキーの発行・失効は、顧客情報の編集権限(update)に相乗りさせています。専用の権限(customers.api-token.manageのようなもの)を新設することもできますが、「顧客を編集できる人なら、その顧客のAPIキーも管理できて当然」という業務ルールなら、既存の権限をそのまま使う方がシンプルです。権限の種類を増やしすぎると、後から見たときに「このリソースにはどんな権限があったか」を把握するコストが増えます。
まとめ
- 顧客ごとに有効なAPIキーは1つだけにし、発行前に必ず古いキーを削除する
- 平文トークンは発行直後の1回しか見えない。フラッシュデータで1回だけ画面に流す
- 発行と失効を「トークン全削除+作るか作らないか」という対称的な実装にする
- 新しい権限を安易に増やさず、既存の権限に相乗りできないか先に検討する
この記事はZennにも投稿しています。