personal_access_tokensのtokenable_typeとtokenable_idをuser_typeとuser_idに変えてトークン発行しようとしたらtokenable_idがないよとSQLに怒られたとき
英文でもヒットしなかったので一応残しておく。
migrationは以下になっている。
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
$table->morphs('user')
に書き換えたい場合もあることだろう。
対象のモデルなどでuse HasApiTokens
しているから$createUser->createToken('my-api-token')
のようにcreateTokenが使えるようになるわけだが、これだけだとtokenable_idなんてないぞとSQLエラーになる。
createTokenの中を追いかけていくと、以下のような箇所がある。
$token = $this->tokens()->create([
'name' => $name,
'token' => hash('sha256', $plainTextToken = Str::random(40)),
'abilities' => $abilities,
]);
tokens()をみてみる。
/**
* Get the access tokens that belong to model.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function tokens()
{
return $this->morphMany(Sanctum::$personalAccessTokenModel, 'tokenable');
}
Relations\MorphMany
の部分を見て、HasManyの拡張かな、と思った。
morphManyメソッドの第二引数でハードコーティングされてるtokenableがあるから、tokenable_idなんてないぞって言われてるんだろうなと推測。
つまりこのtokensメソッドを対象のモデルに定義してオーバーライドしてあげればuser_idとして認識してくれる、、、?
/**
* ユーザーが持っているパーソナルアクセストークン
*
* @return MorphMany
*/
public function tokens(): MorphMany
{
return $this->morphMany(Sanctum::$personalAccessTokenModel, 'user'); // tokenableがデフォルト値なのでuserに上書き。
}
ビンゴ。
もっと深い部分。
/**
* Get the polymorphic relationship columns.
*
* @param string $name
* @param string $type
* @param string $id
* @return array
*/
protected function getMorphs($name, $type, $id)
{
return [$type ?: $name.'_type', $id ?: $name.'_id'];
}
morphManyの第三第四引数が指定されてるならそれを使う。
第三第四引数がnullなら、第二引数の値を使うって感じ。
$this->morphMany(Sanctum::$personalAccessTokenModel, 'user', 'user_type', 'user_id',)
ってのも冗長なので
$this->morphMany(Sanctum::$personalAccessTokenModel, 'user')
が妥当かと。
ちなみになぜtokenable_idが嫌(ダメとは言っていない)なのかというと、このカラムにはユーザーのIDが入る。
DBスキーマ的にリレーション関係にあたるカラム名はテーブル名単体系_id
とすることをルールとしているから。
発行tokenをBearer Tokenにsetしてauth:sanctumミドルウェアを通過しようとしたが"message": "Unauthenticated."となって認証ルートに進めない
前段のtipsを蒸し返すようなtipsになるんですが、personal_access_tokensのtokenable_typeとtokenable_idを変えない方が良いです。
/**
* Retrieve the authenticated user for the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function __invoke(Request $request)
{
foreach (Arr::wrap(config('sanctum.guard', 'web')) as $guard) {
if ($user = $this->auth->guard($guard)->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
}
if ($token = $this->getTokenFromRequest($request)) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
if (! $this->isValidAccessToken($accessToken) ||
! $this->supportsTokens($accessToken->tokenable)) {
return;
}
$tokenable = $accessToken->tokenable->withAccessToken(
$accessToken
);
event(new TokenAuthenticated($accessToken));
if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') &&
method_exists($accessToken->getConnection(), 'setRecordModificationState')) {
tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) {
$accessToken->forceFill(['last_used_at' => now()])->save();
$accessToken->getConnection()->setRecordModificationState($hasModifiedRecords);
});
} else {
$accessToken->forceFill(['last_used_at' => now()])->save();
}
return $tokenable;
}
}
$accessToken->tokenable
の部分なんですが、以下のように引数でtokenable
とハードコーティングされています。
/**
* Get the tokenable model that the access token belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function tokenable()
{
return $this->morphTo('tokenable');
}
これは外部keyにあたるのでuserに変更すると内部でuser_idに変換され、userモデルを引っ張ってきています。
MorphToって書いてますけど、いわばbelogToです。
ここまでわかれば、処理をオーバーライドするだけなのですが、なんにせよprotectedがない...。
そして遡ること、以下の処理にたどり着きます。
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if (app()->runningInConsole()) {
$this->registerMigrations();
$this->publishes([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'sanctum-migrations');
$this->publishes([
__DIR__.'/../config/sanctum.php' => config_path('sanctum.php'),
], 'sanctum-config');
$this->commands([
PruneExpired::class,
]);
}
$this->defineRoutes();
$this->configureGuard();
$this->configureMiddleware();
}
そうです、どうあれ、SanctumServiceProviderをルートリポジトリ側に持ってきてここからオーバーライドし、最終的に$this->morphTo('tokenable')
を$this->morphTo('user')
に変更するところまで処理を引っ張ってこなくてはならない気がしています。
他に方法もあるかもしれないですが、これをやるとしたらリポジトリが相当見苦しくなります。可能な限り無駄は省いた方がいいはずです。
外部key変更対応のためだけにリポジトリが見苦しくなるのを許容するか、このテーブル(personal_access_tokens)だけ外部keyの命名から外れることを許容するか、悩みどころです。
Sanctumを自由自在に操れるほど理解しているならいいかもですが、tokenable_idじゃなくなることで他にも処理に影響が出てくる可能性を考えると変更しない方がいいように感じます。(苦しんで覚える分には効果てきめん)
そもそもSanctumはSanctumでもネイティブアプリじゃないのであればトークン認証じゃなくてSPA認証の方を使えばpersonal_access_tokensテーブルは不要になるのでこういうことで悩まなくて良くなる。