L'uso di Laravel Test 7 e Laravel Passport 9.3 con Personal Access Client dà l'eccezione "Tentativo di ottenere la proprietà 'id' di non oggetto"

Aug 19 2020

Sto progettando uno schema di autenticazione personalizzato (basato su chiavi pubbliche) insieme a un'API senza stato e ho deciso che Passport avrebbe soddisfatto la necessità di richieste di post-autenticazione.

Supponendo che l'autenticazione abbia esito positivo e che l'utente sia autenticato, riceverà un token di accesso personale e utilizzerà il token per tutte le ulteriori richieste. Il problema che sto riscontrando (ancora dopo molte ricerche attraverso vari forum e Stack Overflow) è che quando si utilizza la suite di test integrata di Laravel, sul metodo createToken (), si genera un'eccezione (certamente comune):

"ErrorException: tentativo di ottenere la proprietà 'id' di un oggetto non".

Sono in grado di creare manualmente un utente tramite Tinker e creare un token tramite Tinker. Tuttavia, si verificano problemi durante il tentativo di automatizzare questo processo dopo l'autenticazione .

Di seguito è riportato lo snippet di codice pertinente dopo l'autenticazione:

            Auth::login($user); $user = Auth::user();
            $tokenResult = $user->createToken('Personal Access Token');
            $token = $tokenResult->token;

            $token->expires_at = Carbon::now()->addWeeks(1); $token->save();            

            return response()->json([
                "access_token" => $tokenResult->accessToken, "token_type" => "Bearer", "expires_at" => Carbon::parse( $tokenResult->token->expires_at)->toDateTimeString()
            ],
            200);

Ho chiamato manualmente Auth :: login sull'utente, per assicurarmi che l'utente sia loggato e Auth :: user () restituisce l'utente (non null). All'esecuzione della terza riga di codice, l'eccezione viene generata con la seguente mini-traccia dello stack (posso fornire una traccia dello stack completa se richiesto).

laravel\passport\src\PersonalAccessTokenFactory.php:100
laravel\passport\src\PersonalAccessTokenFactory.php:71
laravel\passport\src\HasApiTokens.php:67
app\Http\Controllers\Auth\LoginController.php:97
laravel\framework\src\Illuminate\Routing\Controller.php:54
laravel\framework\src\Illuminate\Routing\ControllerDispatcher.php:45

Dall'esecuzione di questo tramite il debug alcune volte, anche se la classe viene chiamata e caricata e sembra che il client venga trovato tramite ControllerDispatcher -> Client :: find (id) e trovato in ClientRepository, quando arriva a PersonalAccessTokenFactory, il $client passed in is null (which explains why the $client-> id non può essere trovato, anche se non ho idea del motivo per cui $ client è nullo a questo punto).

protected function createRequest($client, $userId, array $scopes)
{
    $secret = Passport::$hashesClientSecrets ? Passport::$personalAccessClientSecret : $client->secret;

    return (new ServerRequest)->withParsedBody([
        'grant_type' => 'personal_access',
        'client_id' => $client->id,
        ...
}

Cose che ho fatto / provato con alcune indicazioni dalla documentazione e altri post:

  • Creato manualmente un utente in Tinker e creato il token tramite Tinker : funziona .
  • Assicurati che l'utente abbia effettuato l'accesso prima di tentare di generare il token.
  • passport: installa (e aggiunge l'opzione --force)
  • Il client di accesso personale garantito viene generato con passaporto: client --personal
  • Assicurati che AuthServiceProvider :: boot () contenga ClientID e Client Secret (in .env).
  • migrate: refresh seguito da passport: install --force
  • Rimozione completa di Passport, rimozione di tutti i file, chiavi, migrazioni e voci di database, seguita da una migrazione: aggiornamento e reinstallazione di Passport, oltre alla generazione di un client di accesso personale aggiuntivo (anche se ne viene generato uno durante passport: installazione).

Non sono sicuro di dove altro guardare / che altro provare a questo punto, quindi qualsiasi aiuto o guida sarebbe molto apprezzato!

Risposte

grantley Aug 21 2020 at 22:41

Alla fine ho scoperto la soluzione. Il problema è a più livelli, in parte dovuto alla documentazione obsoleta di Laravel per quanto riguarda i test e i client Passport Personal Access.

La prima parte del problema aveva a che fare con l'utilizzo del tratto RefreshDatabase nel mio unit test. Poiché questo crea un database fittizio con set di dati vuoti, sebbene i client stessi esistano nel database reale e nel file .env, quando viene eseguito il test, il test non vede quei client come esistenti nel database fittizio. Per risolvere questo problema, è necessario creare un client nella funzione di configurazione prima di eseguire il test.

public function setUp() : void
{
    parent::setUp();
    $this->createClient(); //Private method->Full code below
}

Ciò risolve il problema relativo alla presenza di un client nullo durante il test, ma a partire da Laravel 7, Laravel ha aggiunto il requisito per i client di accesso personale che l'id e il segreto del client devono essere conservati all'interno del file .env. Durante l'esecuzione del test, il test vedrà l'effettivo ID client e segreto nel file .env e non riuscirà a convalidarli con il client che è stato creato e archiviato nel database fittizio, restituendo un'altra eccezione: "Client Authentication Failed".

La soluzione a questo problema è creare un file .env.testing nella directory principale del progetto, copiandovi il contenuto del file .env e assicurandovi che le chiavi seguenti esistano con i valori per il vostro client di accesso personale creato principale, o copiando il segreto da un client generato solo per test (consiglierei quest'ultimo).

 PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1

 PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

Quindi, utilizzando il codice riportato di seguito, assicurati che il valore $ clientSecret sia uguale al valore della chiave nel file .env.testing.

private function createClient() : void
{
    $clientRepository = new ClientRepository(); $client = $clientRepository->createPersonalAccessClient( null, 'Test Personal Access Client', 'http://localhost' ); DB::table('oauth_personal_access_clients')->insert([ 'client_id' => $client->id,
        'created_at' => new DateTime,
        'updated_at' => new DateTime,
    ]);

    $clientSecret = 'unhashed-client-secret-value'; $client->setSecretAttribute($clientSecret); $client->save();
}

Questo creerà un nuovo client, imposterà il segreto dell'attributo al valore nella variabile e aggiornerà il segreto del database fittizio per contenere lo stesso valore. Si spera che questo aiuti chiunque abbia lo stesso problema.