Laravel 8のAPI認証に味付けする - Laravel Sanctum - 無解説メモ
内容
- sanctumのAPI認証の有効期限の検討のメモ
実現方法(案)
- sanctum標準
- 拡張A:有効期限保持/トークン認証コールバック
- 拡張B:有効期限保持/有効期限切れメッセージ返却
(1) sanctum標準
設定方法
config/sanctum.php
のexpiration
に有効期限値(単位:分)を指定することで、トークンに有効期限を付与できる。
処理内容
Laravel\Sanctum\Guard
のprotected function isValidAccessToken($accessToken): bool
で、personal_access_tokens
レコードのcreated_at
の値とexpiration
設定値を考慮した時間の比較結果を元に、「APIトークンが有効か無効か判定」している。判定結果はboolであり、APIトークンが無効である場合にその原因は考慮しない。
有効期限切れの場合、HTTPステータス401 Unauthorized
で、メッセージ「Unauthenticated.」が返却される。
個人的要求
- 作成時刻で制御するのではなく、有効期限を保持しその値で制御を行いたい。
- APIトークンが破棄された場合などAPIトークン自体が無効なケースと、APIトークンの有効期限が切れた場合の制御を分けたい。
(2) 拡張A
拡張概要
-
方針
- 可能な限りSanctumの仕様に準拠する
- Sanctumの
expiration
を利用せずに有効期限チェックを行う
-
概要
-
personal_access_tokens
テーブルに有効期限expires_at
を追加 - 有効期限は、APIトークンのレコード登録時に記録
- 有効期限チェックに
Sanctum::authenticateAccessTokensUsing(callable $callback)
を利用
-
-
補足
- 前回の記事で作成したプロジェクトを拡張
手順1. personal_access_tokens拡張
1. マイグレーションファイル作成
php artisan make:migration add_columns_to_personal_access_tokens
※ ファイル名の時刻をシーケンス番号に修正
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddColumnsToPersonalAccessTokens extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->timestamp('token_expires_at')->nullable()->after('token');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->dropColumn('token_expires_at');
});
}
}
2. マイグレーション
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate
Migrating: 2021_09_23_000001_add_columns_to_personal_access_tokens
Migrated: 2021_09_23_000001_add_columns_to_personal_access_tokens (14.92ms)
参考:実行前のマイグレーションステータス
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate:status
+------+---------------------------------------------------------+-------+
| Ran? | Migration | Batch |
+------+---------------------------------------------------------+-------+
| Yes | 2014_10_12_000000_create_users_table | 1 |
| Yes | 2014_10_12_100000_create_password_resets_table | 1 |
| Yes | 2019_08_19_000000_create_failed_jobs_table | 1 |
| Yes | 2019_12_14_000001_create_personal_access_tokens_table | 1 |
| No | 2021_09_23_000001_add_columns_to_personal_access_tokens | |
+------+---------------------------------------------------------+-------+
参考:実行後のマイグレーションステータス
laradock@d79ca8e4255f:/var/www/example-app01$ php artisan migrate:status
+------+---------------------------------------------------------+-------+
| Ran? | Migration | Batch |
+------+---------------------------------------------------------+-------+
| Yes | 2014_10_12_000000_create_users_table | 1 |
| Yes | 2014_10_12_100000_create_password_resets_table | 1 |
| Yes | 2019_08_19_000000_create_failed_jobs_table | 1 |
| Yes | 2019_12_14_000001_create_personal_access_tokens_table | 1 |
| Yes | 2021_09_23_000001_add_columns_to_personal_access_tokens | 2 |
+------+---------------------------------------------------------+-------+
手順2. HasApiTokensトレイト拡張
Userモデルに適用されているHasApiTokens
トレイトのcreateToken
メソッドを置き換えるためトレイトを拡張
1. 拡張トレイト作成
app/Http/Traits/HasApiTokens.php
ファイルを手動で作成
<?php
namespace App\Http\Traits;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens as BaseHasApiTokens;
use Laravel\Sanctum\NewAccessToken;
trait HasApiTokens
{
use BaseHasApiTokens {
BaseHasApiTokens::createToken as base_createToken;
}
/**
* Create a new personal access token for the user.
*
* @param string $name
* @param array $abilities
* @return \Laravel\Sanctum\NewAccessToken
*/
public function createToken(string $name, array $abilities = ['*'])
{
// ToDo config化
$expiresAt = now()->addDay();
$token = $this->tokens()->create([
'name' => $name,
'token' => hash('sha256', $plainTextToken = Str::random(40)),
'abilities' => $abilities,
'token_expires_at' => $expiresAt,
]);
return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}
}
2. HasApiTokensトレイトを置き換え
Userモデルで適用しているHasApiTokensを拡張したトレイトに置き換える
:
//use Laravel\Sanctum\HasApiTokens;
use App\Http\Traits\HasApiTokens;
:
手順3. PersonalAccessTokenクラス拡張
1. PersonalAccessToken拡張クラス作成
php artisan make:model Sanctum/PersonalAccessToken
<?php
namespace App\Models\Sanctum;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'token_expires_at' => 'datetime',
'abilities' => 'json',
'last_used_at' => 'datetime',
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'token',
'token_expires_at',
'abilities',
];
/**
* アクセストークンの有効性を独自チェックする
*
* @param mixed $accessToken
* @param bool $isValid
* @return bool
*/
public static function isValidAccessToken($accessToken, bool $isValid)
{
if (!$accessToken->token_expires_at) {
return $isValid;
}
return $accessToken->token_expires_at->gt(now());
}
}
2. 拡張PersonalAccessTokenクラスの適用
AppServiceProviderのboot()にカスタムモデルの適用とアクセストークン認証のコールバックの適用を記述
:
use Laravel\Sanctum\Sanctum;
use App\Models\Sanctum\PersonalAccessToken;
:
public function boot()
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Sanctum::authenticateAccessTokensUsing([PersonalAccessToken::class, 'isValidAccessToken']);
}
:
手順4. その他調整
1. APIトークンと共に有効期限を返却
:
// return response()->json(['api_token' => $token->plainTextToken], 200);
return response()->json(['api_token' => $token->plainTextToken, 'expires_at' => $token->accessToken->token_expires_at], 200);
:
結果考察
Sanctum::authenticateAccessTokensUsing(callable $callback)
により、独自のトークンチェックを実装できるため、これにより、作成時刻ではなく有効期限で制御することはできる。
しかしながら、有効期限切れの場合と、他のトークン無効を区別することはできない。
Sanctumとしては、ここまでで十分でしょ、ということかな。
(3) 拡張B
拡張概要
-
拡張Aの有効期限の記録のみを適用する(
Sanctum::authenticateAccessTokensUsing(callable $callback)
を利用しない) -
SanctumのAPI認証は有効期限チェックは行わず一度完結させる
-
API認証通過後、別途ミドルウェアでAPIトークンの有効期限をチェックする
-
補足
- (2) 拡張A実施後のプロジェクトを拡張
手順1. 適用した有効期限チェックを削除
1. Sanctum::authenticateAccessTokensUsingのコメントアウト
:
public function boot()
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
// Sanctum::authenticateAccessTokensUsing([PersonalAccessToken::class, 'isValidAccessToken']);
}
:
手順2. 有効期限チェック処理を実装
1. PersonalAccessTokenに有効期限チェックメソッド追加
:
/**
* アクセストークンの有効期限が失効しているかチェックする
*
* @param mixed $accessToken
* @param bool $isValid
* @return bool
*/
public static function isExpiredToken($accessToken)
{
if (!$accessToken->token_expires_at) {
// nullの場合は失効扱い
return true;
}
return $accessToken->token_expires_at->lte(now());
}
:
手順3. 有効期限をチェックするミドルウェアを設置
1. VerifyExpiredApiTokenミドルウェア作成
php artisan make:middleware VerifyExpiredApiToken
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
class VerifyExpiredApiToken
{
/**
* チェックを除外するリクエストURLのパターンを指定
* @var array
*/
protected $except = [
'api/authenticate',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @return mixed
*
* @throws AuthenticationException
*/
public function handle($request, Closure $next)
{
if (!$request->is($this->except)) {
$user = $request->user();
$token = !!$user ? $user->currentAccessToken() : null;
if (!$token || $token->isExpiredToken($token)) {
throw new AuthenticationException('API token expired.');
}
}
return $next($request);
}
}
2. VerifyExpiredApiTokenミドルウェア適用
HttpのKernel設定で、
apiのmiddelewareGroupsに作成したミドルウェアを追加
:
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\VerifyExpiredApiToken::class,
],
:
結果考察
Sanctumの機能で実現することをこだわらなければ、ミドルウェアで何とでもなる。
だからこそ、Sanctumの仕様はややこしくしないように簡単な有効期限チェックまでに留めているのでしょうね。
改めて、ミドルウェアが便利、というところで。
今回はここまで。おつかれさまでした。
Discussion