Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

bmpf's avatar
Level 3

Laravel Sanctum personal_access_tokens record user agent and ip with last_used_at timestamp

In laravel Sanctum is possible to modify the 'auth:sanctum' middleware to record the user agent and ip when the last_used_at timestamp is updated ?

I created a middlware called SaveIpAndUserAgentInEveryRequest , and it is working , but the database is writen twice, one for the last_used_at and then another for the SaveIpAndUserAgentInEveryRequest .

Code.

Api Routes

api.php

Route::middleware(['auth:sanctum'])->group(function () {

    Route::get('/me', function (Request $request) {
        return $request->user();
    });
});

Kernel:


        'api' => [
            //\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\SaveIpAndUserAgentInEveryRequest::class,
        ],


SaveIpAndUserAgentInEveryRequest

<?php

namespace App\Http\Middleware;

use App\Models\PersonalAccessToken;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class SaveIpAndUserAgentInEveryRequest
{
    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param \Closure(Request): (Response) $next
     * @return Response
     */
    public function handle(Request $request, Closure $next): Response
    {
        // Check if the request is authenticated
        if ($request->user()) {
            // Access the authenticated user
            $user = $request->user();

            // Access the user's token
            $token = $user->currentAccessToken();

            // Update token data if a token exists
            if ($token instanceof PersonalAccessToken) {
                $token->saveIpAndUserAgent();
            }
        }

        return $next($request);
    }
}

Table:

array:12 [ // app/Http/Middleware/SaveIpAndUserAgentInEveryRequest.php:33
  "id" => 15
  "tokenable_type" => "App\Models\User"
  "tokenable_id" => "9b699bb2-aafa-4281-b...."
  "name" => "api-token"
  "abilities" => array:1 [
    0 => "*"
  ]
  "ip_address" => "192.168.65.1"
  "user_agent" => "PostmanRuntime/7.36.3"
  "last_used_at" => "2024-02-24T13:30:09.000000Z"
  "expires_at" => null
  "created_at" => "2024-02-24T13:08:45.000000Z"
  "updated_at" => "2024-02-24T13:30:09.000000Z"
  "tokenable" => array:8 [
    "id" => "9b699bb...."
    "name" => "My name"
    "email" => "e....."
    "phone" => null
    "role" => "user"
    "email_verified_at" => null
    "created_at" => "2024-02-24T12:47:40.000000Z"
    "updated_at" => "2024-02-24T12:47:40.000000Z"
  ]
]

Migration

        Schema::create('personal_access_tokens', function (Blueprint $table) {
            $table->id();
            //$table->morphs('tokenable');
            $table->uuidMorphs('tokenable');
            $table->string('name');
            $table->string('token', 64)->unique();
            $table->text('abilities')->nullable();
            $table->text('ip_address')->nullable();
            $table->text('user_agent')->nullable();
            $table->timestamp('last_used_at')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();
        });

WORKAROUND SO FAR:

<?php

namespace App\Models;

use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
{

    protected $fillable = [
        'name',
        'token',
        'abilities',
        'expires_at',
        'ip_address',
        'user_agent',
        'last_used_at',
    ];


    public function saveIpAndUserAgent() :void
    {
        $this->update(['ip_address' => request()->ip(), 'user_agent' => request()->userAgent()]);
    }


    /**
	* WORKAROUND , when there is a save method called in this model, check if last_used_at is the only key to be updated, if so, add ip_address & user_agent  too.
     * @param  array  $options
     * @return bool
     */
    public function save(array $options = []): bool
    {
        $changes = $this->getDirty();

        if (!array_key_exists('last_used_at', $changes) || count($changes) > 2) {
            return parent::save();
        }

        // Update last_used_at with ip_address and user_agent

        $this->ip_address = request()->ip();
        $this->user_agent = request()->userAgent();

        return parent::save();
    }
}

0 likes
4 replies
haim's avatar

I am not sure, but I see that you created your code inside a Middleware. Maybe because it inside a middleware, it is being called twice. One time when Sanctum checks for middleware, and second time when you call it from your own code with $token->saveIpAndUserAgent();

So maybe don't load it the way you load it now inside the Kernel. Instead try calling this: $token->saveIpAndUserAgent(); from the route api call, or the controller that controls the route requests.

1 like
bmpf's avatar
Level 3

@haim Is not that, by default laravel updates last_used_at automatically. I want to change the behavior, and update last_used_at + ip_address + user_agent (in one transaction) inside the personal_access_tokens table.

bmpf's avatar
Level 3

@amitsolanki24_

Yes:,

    public function saveIpAndUserAgent() :void
    {
        $this->update(['ip_address' => request()->ip(), 'user_agent' => request()->userAgent()]);
    }

Please or to participate in this conversation.