📝

Laravel 8のAPI認証に味付けする - Laravel Sanctum - 無解説メモ

2021/09/26に公開

内容

  • sanctumのAPI認証の有効期限の検討のメモ

実現方法(案)

  1. sanctum標準
  2. 拡張A:有効期限保持/トークン認証コールバック
  3. 拡張B:有効期限保持/有効期限切れメッセージ返却

(1) sanctum標準

設定方法

config/sanctum.phpexpirationに有効期限値(単位:分)を指定することで、トークンに有効期限を付与できる。

処理内容

Laravel\Sanctum\Guardprotected 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

※ ファイル名の時刻をシーケンス番号に修正

database/migrations/yyyy_mm_dd_000001_add_columns_to_personal_access_tokens.php
<?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ファイルを手動で作成

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を拡張したトレイトに置き換える

app/Models/User.php
//use Laravel\Sanctum\HasApiTokens;
use App\Http\Traits\HasApiTokens;

手順3. PersonalAccessTokenクラス拡張

1. PersonalAccessToken拡張クラス作成

php artisan make:model Sanctum/PersonalAccessToken
app/Models/Sanctum/PersonalAccessToken.php
<?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()にカスタムモデルの適用とアクセストークン認証のコールバックの適用を記述

app/Providers/AppServiceProvider.php
use Laravel\Sanctum\Sanctum;
use App\Models\Sanctum\PersonalAccessToken;public function boot()
    {
        Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
        Sanctum::authenticateAccessTokensUsing([PersonalAccessToken::class, 'isValidAccessToken']);
    }

手順4. その他調整

1. APIトークンと共に有効期限を返却

app/Http/Controllers/Api/AuthController.php
//            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のコメントアウト

app/Providers/AppServiceProvider.php
public function boot()
    {
        Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
//        Sanctum::authenticateAccessTokensUsing([PersonalAccessToken::class, 'isValidAccessToken']);
    }

手順2. 有効期限チェック処理を実装

1. PersonalAccessTokenに有効期限チェックメソッド追加

app/Models/Sanctum/PersonalAccessToken.php
/**
     * アクセストークンの有効期限が失効しているかチェックする
     *
     * @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
app/Http/Middleware/VerifyExpiredApiToken.php
<?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に作成したミドルウェアを追加

app/Http/Kernenl.php
'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\VerifyExpiredApiToken::class,
        ],

結果考察

Sanctumの機能で実現することをこだわらなければ、ミドルウェアで何とでもなる。
だからこそ、Sanctumの仕様はややこしくしないように簡単な有効期限チェックまでに留めているのでしょうね。
改めて、ミドルウェアが便利、というところで。

今回はここまで。おつかれさまでした。

Discussion