L'utilisation de Laravel Test 7 et Laravel Passport 9.3 avec Personal Access Client donne l'exception «Essayer d'obtenir la propriété 'id' d'un non-objet»

Aug 19 2020

Je conçois un schéma d'authentification personnalisé (basé sur des clés publiques) parallèlement à une API sans état, et j'ai décidé que Passport répondrait au besoin de demandes de post-authentification.

En supposant que l'authentification réussit et que l'utilisateur est authentifié, il recevra un jeton d'accès personnel et l'utilisera pour toutes les autres demandes. Le problème que je rencontre (toujours après de nombreuses recherches dans divers forums et Stack Overflow) est que lorsque vous utilisez la suite de tests intégrée de Laravel, sur la méthode createToken (), elle génère une exception (certes commune):

"ErrorException: Essayer d'obtenir la propriété 'id' d'un non-objet".

Je suis capable de créer manuellement un utilisateur via Tinker et de créer un jeton via Tinker. Cependant, je rencontre des problèmes lors de la tentative d'automatisation de ce processus après l'authentification .

Voici l'extrait de code pertinent après l'authentification:

            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);

J'ai appelé manuellement Auth :: login sur l'utilisateur, pour m'assurer que l'utilisateur est connecté, et Auth :: user () renvoie l'utilisateur (non nul). Lors de l'exécution de la troisième ligne de code, l'exception est levée avec la mini-trace de pile suivante (je peux fournir une trace de pile complète si demandé).

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

De l'exécuter via le débogage à quelques reprises - même si la classe est appelée et chargée, et qu'il semble que le client soit trouvé via ControllerDispatcher -> Client :: find (id) et trouvé dans ClientRepository, quand il arrive à PersonalAccessTokenFactory, le $client passed in is null (which explains why the $client-> id est introuvable, même si je n'ai aucune idée de la raison pour laquelle $ client est nul à ce stade).

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,
        ...
}

Ce que j'ai fait / essayé avec quelques conseils de la documentation et d'autres articles:

  • Création manuelle d'un utilisateur dans Tinker et création du jeton via Tinker - cela fonctionne .
  • Assurez-vous que l'utilisateur est connecté avant d'essayer de générer un jeton.
  • passeport: installer (et ajouter l'option --force)
  • Garanti que le client d'accès personnel est généré avec le passeport: client - personnel
  • Assuré que AuthServiceProvider :: boot () contient le ClientID et Client Secret (dans le .env).
  • migrer: actualiser suivi de passeport: installer --force
  • Suppression complète de Passport, suppression de tous les fichiers, clés, migrations et entrées de base de données, suivie d'une migration: actualisation et réinstallation de Passport, ainsi que génération d'un client d'accès personnel supplémentaire (même s'il est généré pendant le passeport: installation).

Je ne sais pas où chercher d'autre / quoi essayer d'autre à ce stade, donc toute aide ou conseil serait très apprécié!

Réponses

grantley Aug 21 2020 at 22:41

J'ai finalement découvert la solution. Le problème est à plusieurs niveaux, en partie lié à la documentation Laravel obsolète en ce qui concerne les tests et les clients Passport Personal Access.

La première partie du problème concernait l'utilisation du trait RefreshDatabase sur mon test unitaire. Étant donné que cela crée une base de données fictive avec des ensembles de données vides, bien que les clients eux-mêmes existent dans la base de données réelle et le fichier .env, lorsque le test est exécuté, le test ne voit pas ces clients comme existant dans la base de données fictive. Pour résoudre ce problème, vous devez créer un client dans la fonction de configuration avant l'exécution du test.

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

Cela résout le problème d'avoir un client nul pendant le test, mais à partir de Laravel 7, Laravel a ajouté une exigence pour les clients d'accès personnel selon laquelle l'id et le secret du client doivent être conservés dans le fichier .env. Lors de l'exécution du test, le test verra l'ID client et le secret réels dans le .env, et ne parviendra pas à les valider avec le client qui a été créé et stocké dans la base de données fictive, renvoyant une autre exception: «L'authentification du client a échoué».

La solution à ce problème consiste à créer un fichier .env.testing dans le répertoire de votre projet principal, en y copiant le contenu de votre fichier .env et en vous assurant que les clés ci-dessous existent avec des valeurs pour votre client d'accès personnel principal créé, ou en copiant le secret à partir d'un client généré juste pour le test (je conseillerais ce dernier).

 PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1

 PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

Ensuite, en utilisant le code ci-dessous, assurez-vous que la valeur $ clientSecret est la même que la valeur de clé dans votre fichier .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();
}

Cela créera un nouveau client, définira l'attribut secret sur la valeur de la variable et mettra à jour le secret de la base de données fictive pour qu'il contienne la même valeur. Espérons que cela aide n'importe qui avec le même problème.