El uso de Laravel Test 7 y Laravel Passport 9.3 con Personal Access Client da la excepción "Intentando obtener la propiedad 'id' de un no objeto"

Aug 19 2020

Estoy diseñando un esquema de autenticación personalizado (basado en claves públicas) junto con una API sin estado, y decidí que Passport satisfaría la necesidad de solicitudes posteriores a la autenticación.

Suponiendo que la autenticación sea exitosa y que el usuario esté autenticado, recibirán un token de acceso personal y usarán el token para todas las solicitudes posteriores. El problema que estoy experimentando (todavía después de mucho buscar en varios foros y Stack Overflow) es que cuando utilizo el conjunto de pruebas integrado de Laravel, en el método createToken (), genera una excepción (ciertamente común):

"ErrorException: tratando de obtener la propiedad 'id' de un no objeto".

Puedo crear manualmente un usuario a través de Tinker y crear un token a través de Tinker. Sin embargo, tengo problemas al intentar automatizar este proceso después de la autenticación .

Aquí está el fragmento de código relevante posterior a la autenticación:

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

He llamado manualmente a Auth :: login en el usuario, para asegurarme de que el usuario ha iniciado sesión, y Auth :: user () devuelve el usuario (no nulo). Al ejecutar la tercera línea de código, la excepción se lanza con el siguiente mini seguimiento de pila (puedo proporcionar un seguimiento de pila completo si se solicita).

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

Desde ejecutar esto hasta la depuración varias veces, aunque la clase se llama y se carga, y parece que el Cliente se encuentra a través de ControllerDispatcher -> Client :: find (id) y se encuentra en ClientRepository, cuando llega a PersonalAccessTokenFactory, el $client passed in is null (which explains why the $client-> id no se puede encontrar, aunque no tengo idea de por qué $ client es nulo en este momento).

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

Cosas que he hecho / probado con alguna orientación de la documentación y otras publicaciones:

  • Creé manualmente un usuario en Tinker y creó el token a través de Tinker; esto funciona .
  • Se aseguró de que el usuario haya iniciado sesión antes de intentar generar el token.
  • pasaporte: instalar (y agregar la opción --force)
  • El cliente de acceso personal garantizado se genera con pasaporte: cliente - personal
  • Se aseguró de que AuthServiceProvider :: boot () contenga el ClientID y el secreto del cliente (en el .env).
  • migrate: refresh seguido de passport: install --force
  • Eliminación completa de Passport, eliminación de todos los archivos, claves, migraciones y entradas de la base de datos, seguida de una migración: actualización y reinstalación de Passport, junto con la generación de un cliente de acceso personal adicional (aunque se genere uno durante la instalación de passport:).

No estoy seguro de dónde más buscar / qué más probar en este momento, ¡así que cualquier ayuda u orientación sería muy apreciada!

Respuestas

grantley Aug 21 2020 at 22:41

Finalmente descubrí la solución. El problema tiene varias capas, en parte debido a la documentación obsoleta de Laravel en lo que respecta a las pruebas y los clientes de acceso personal de Passport.

La primera parte del problema tenía que ver con el uso del rasgo RefreshDatabase en mi prueba unitaria. Dado que esto crea una base de datos simulada con conjuntos de datos vacíos, aunque los propios clientes existen en la base de datos real y el archivo .env, cuando se ejecuta la prueba, la prueba no ve a esos clientes como existentes en la base de datos simulada. Para resolver este problema, debe crear un cliente en la función de configuración antes de ejecutar la prueba.

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

Esto resuelve el problema de tener un cliente nulo durante la prueba, pero a partir de Laravel 7, Laravel agregó un requisito para los Clientes de acceso personal de que la identificación y el secreto del cliente deben mantenerse dentro del archivo .env. Al ejecutar la prueba, la prueba verá la identificación real del cliente y el secreto en el .env, y no podrá validarlos con el cliente que se creó y almacenó en la base de datos simulada, devolviendo otra excepción: "Error de autenticación del cliente".

La solución a este problema es crear un archivo .env.testing en el directorio principal de su proyecto, copiando el contenido del archivo .env en él y asegurándose de que las claves a continuación existan con valores para su cliente de acceso personal principal creado o copiando el secreto de un cliente generado solo para probar (yo recomendaría este último).

 PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1

 PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

Luego, usando el código siguiente, asegúrese de que el valor de $ clientSecret sea el mismo que el valor clave en su archivo .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();
}

Esto creará un nuevo cliente, establecerá el atributo secreto en el valor de la variable y actualizará el secreto de la base de datos simulada para que contenga el mismo valor. Con suerte, esto ayudará a cualquiera que tenga el mismo problema.