1

I have low experience with token authentication and sanctum.

General Context: I’m currently migrating a framework of mine to Laravel and still in the early stages. I know that Laravel has its own database construction mechanism that is recommended to use the migrations and the wired up Models, however, for my purpose, I’d like to use my own database (without migrations) that I use in other systems that I’ve built in the past. The idea for this system is to have a shared database, but operable through different tech stacks.

Basic Configuration:

  • Laravel 8.37
  • Sanctum 2.14
  • PHP 8

I had opened a previous question and made some progress with it: Laravel Authentication API Sanctum – With Custom Database

The Goal: Basically, I´d like to create a token from one controller using my custom model.

Controller that is creating the token:

$oudRecord = new UsersDetails($oudRecordParameters);
$oudRecordData = $oudRecord->cphBodyBuild();
$oudRecordToken = $oudRecord->createToken('testing_token')->plainTextToken;

In my custom model, I’ve manage to override the createToken() already. However, from my previous interaction in the first question I posted, I know that I´ll need to override the tokens() method too. That’s where I’m stuck now.

The custom model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\Sanctum;


class UsersDetails extends Authenticatable
{
    use HasFactory, HasApiTokens;

    // Properties.
    // ----------------------
    private float $idTbUsers = 0;
    private array|null $arrSearchParameters = null;

    private int $terminal = 0; // terminal: 0 - admin | 1 - frontend
    
    private string $labelPrefix = 'backend';

    private array|null $arrSpecialParameters = null;

    private array|null $resultsUsersDetails = null; 
    private array|null $oudRecordParameters = null;

    protected mixed $objUsersDetails = null; 
    protected array|null $arrUsersListing = null;
    // ----------------------

    /**
     * Constructor.
     * @param ?array $_oudRecordParameters
     */
    public function __construct(?array $_oudRecordParameters = null)
    {
        if ($_oudRecordParameters !== null) {
            $this->oudRecordParameters = $_oudRecordParameters;
        }

        if ($this->terminal === 1) {
            $this->labelPrefix = 'frontend';
        }
    }

    /**
     * Build content placeholder body.
     * @return array
     */
    public function cphBodyBuild(): array
    {
        // Variables.
        // ----------------------
        $arrReturn = ['returnStatus' => false];
        // ----------------------

        // Logic.
        try {
            // Build object - details.
            if ($this->oudRecordParameters !== null) {
                $oudRecord = new \SyncSystemNS\ObjectUsersDetails($this->oudRecordParameters);
                $arrReturn['oudRecord'] = $oudRecord->recordDetailsGet(0, 1);

                if ($arrReturn['oudRecord']['returnStatus'] === true) {
                    $arrReturn['returnStatus'] = true;
                }
            }
        } catch (Error $cphBodyBuildError) {
            if ($GLOBALS['configDebug'] === true) {
                throw new Error('cphBodyBuildError: ' . $cphBodyBuildError->message());
            }
        } finally {
            //
        }
        
        return $arrReturn;
    }
    

    public function tokens()
    {
        return $this->morphMany(Sanctum::$personalAccessTokenModel, 'tokenable', "tokenable_type", "tokenable_id");
    }

    /**
     * Override createToken method.
     */
    public function createToken(string $name, array $abilities = ['*'])
    {
        $token = $this->tokens()->create([
            'name' => $name,
            'token' => hash('sha256', $plainTextToken = Str::random(80)),
            'abilities' => $abilities,
        ]);
    
        return new NewAccessToken($token, $token->id.'|'.$plainTextToken);
    }    
}

I´m getting the following error now:

local.ERROR: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'tokenable_id' cannot be null (SQL: insert into `personal_access_tokens` (`name`, `token`, `abilities`, `tokenable_id`, `tokenable_type`, `updated_at`, `created_at`) values (testing_token, 51feaf295d0fc9170bb25cf09160c325a9d685477ef4d2aa6226ed9e4085ac66, ["*"], ?, App\Models\UsersDetails, 2023-03-11 13:16:59, 2023-03-11 13:16:59)) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'tokenable_id' cannot be null (SQL: insert into `personal_access_tokens` (`name`, `token`, `abilities`, `tokenable_id`, `tokenable_type`, `updated_at`, `created_at`) values (testing_token, 51feaf295d0fc9170bb25cf09160c325a9d685477ef4d2aa6226ed9e4085ac66, [\"*\"], ?, App\\Models\\UsersDetails, 2023-03-11 13:16:59, 2023-03-11 13:16:59)) at …

I sense that I must, somehow, provide values for some of the fields that Laravel Sanctume tried to record in the database, but I have no idea how to do that? Would it be by overriding properties? Or using another structure in token() override method?

Thanks!

Jorge Mauricio
  • 411
  • 6
  • 18
  • where are you actually saving the User? you can't create a token for a user that doesn't exist yet, it wouldn't have an 'id' ... if that is supposed to be a Model your constructor wrong – lagbox Mar 11 '23 at 14:42
  • The user is already saved in the database. This model just reads the saved user by the user id: ```private float $idTbUsers = 0;``` which is passed by the ```$oudRecordParameters``` in the instance creation. – Jorge Mauricio Mar 11 '23 at 14:57
  • 1
    there are no attributes .. Models have attributes for their fields, not properties ... so there is no 'id' attribute to use for the relationship, hence `tokenable_id` is `null` because the User "Model" doesn't have an id ... this really isn't an Eloquent Model – lagbox Mar 11 '23 at 14:58
  • Is there a way so I can create these necessary attributes in my model, with the values I want, so it can be used in the tokens() method? (BTW: I´m relatively new to Laravel - that´s why the dumb questions) – Jorge Mauricio Mar 11 '23 at 15:03
  • Hey! Seems it worked! I researched a bit in google for creating custom attributes for models and I added this to the my model to test ```protected $attributes = array( 'id' => 1234, ); ``` You have no idea how long I´m stuck with this. Post an answer and I´ll mark it as correct. – Jorge Mauricio Mar 11 '23 at 15:12

1 Answers1

1

try it:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\PersonalAccessToken;

class UsersDetails extends Authenticatable
{
    use HasFactory, HasApiTokens;

    // Properties.
    // ----------------------
    private float $idTbUsers = 0;
    private array|null $arrSearchParameters = null;
    private int $terminal = 0; // terminal: 0 - admin | 1 - frontend
    private string $labelPrefix = 'backend';
    private array|null $arrSpecialParameters = null;
    private array|null $resultsUsersDetails = null;
    private array|null $oudRecordParameters = null;
    protected mixed $objUsersDetails = null;
    protected array|null $arrUsersListing = null;
    // ----------------------

    /**
     * Constructor.
     * @param ?array $_oudRecordParameters
     */
    public function __construct(?array $_oudRecordParameters = null)
    {
        if ($_oudRecordParameters !== null) {
            $this->oudRecordParameters = $_oudRecordParameters;
        }

        if ($this->terminal === 1) {
            $this->labelPrefix = 'frontend';
        }
    }

    /**
     * Build content placeholder body.
     * @return array
     */
    public function cphBodyBuild(): array
    {
        // Variables.
        // ----------------------
        $arrReturn = ['returnStatus' => false];
        // ----------------------

        // Logic.
        try {
            // Build object - details.
            if ($this->oudRecordParameters !== null) {
                $oudRecord = new \SyncSystemNS\ObjectUsersDetails($this->oudRecordParameters);
                $arrReturn['oudRecord'] = $oudRecord->recordDetailsGet(0, 1);

                if ($arrReturn['oudRecord']['returnStatus'] === true) {
                    $arrReturn['returnStatus'] = true;
                }
            }
        } catch (Error $cphBodyBuildError) {
            if ($GLOBALS['configDebug'] === true) {
                throw new Error('cphBodyBuildError: ' . $cphBodyBuildError->message());
            }
        } finally {
            //
        }
        
        return $arrReturn;
    }

    /**
     * Override getAuthIdentifier() method to return the user ID.
     * @return mixed
     */
    public function getAuthIdentifier()
    {
        return $this->idTbUsers;
    }

    /**
     * Override createToken method.
     */
public function createToken(string $name, array $abilities = ['*'], $userId = null)
{
    $userId = $userId ?? $this->getKey();

    $token = $this->tokens()->create([
        'name' => $name,
        'token' => hash('sha256', $plainTextToken = Str::random(80)),
        'abilities' => $abilities,
        'tokenable_id' => $userId,
        'tokenable_type' => get_class($this),
    ]);
 
    return new NewAccessToken($token, $token->id.'|'.$plainTextToken);
}
Ali Sharifi Neyestani
  • 4,162
  • 1
  • 14
  • 22
  • Tried it. Still returns the error: Integrity constraint violation: 1048 Column 'tokenable_id' cannot be null – Jorge Mauricio Mar 11 '23 at 16:59
  • The error message suggests that the tokenable_id column in the personal_access_tokens table cannot be null. The tokenable_id column is the foreign key that references the id column of the UsersDetails model, so it needs to be set to a valid value when creating a token. – Ali Sharifi Neyestani Mar 12 '23 at 08:39
  • To set the tokenable_id value in the createToken method, you need to pass the id of the current user as an argument to the method. I updated createToken method – Ali Sharifi Neyestani Mar 12 '23 at 08:39
  • 1
    It did work. Strange that I had tested before just making something like ```'tokenable_type' => 123,``` and it didn´t work, don´t know exactly why. Anyway, just a tip: last line should be ```return new \Laravel\Sanctum\NewAccessToken($token, $token->id.'|'.$plainTextToken);``` or import the class into the model. And if anyone want´s to follow the other approach I mentioned in my comment suggested by @lagbox, can find in this link which was my first question: https://stackoverflow.com/a/75707581/2510785 – Jorge Mauricio Mar 12 '23 at 15:35