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"
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
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.