Usar o Laravel Test 7 e o Laravel Passport 9.3 com o cliente de acesso pessoal dá a exceção “Tentando obter a propriedade 'id' do não-objeto”

Aug 19 2020

Estou projetando um esquema de autenticação personalizado (baseado em chaves públicas) junto com uma API sem estado e decidi que o Passport atenderia à necessidade de solicitações pós-autenticação.

Supondo que a autenticação seja bem-sucedida e o usuário seja autenticado, ele receberá um token de acesso pessoal e usará o token para todas as solicitações futuras. O problema que estou tendo (ainda depois de muito pesquisar em vários fóruns e Stack Overflow) é que ao usar o pacote de testes embutido do Laravel, no método createToken (), ele gera uma exceção (reconhecidamente comum):

"ErrorException: Tentando obter a propriedade 'id' do não objeto".

Consigo criar manualmente um usuário por meio do Tinker e criar um token por meio do Tinker. No entanto, estou tendo problemas ao tentar automatizar esse processo após a autenticação .

Aqui está o snippet de código relevante pós-autenticação:

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

Eu chamei manualmente Auth :: login no usuário, para garantir que o usuário esteja conectado, e Auth :: user () retorna o usuário (não nulo). Ao executar a terceira linha de código, a exceção é lançada com o seguinte mini rastreamento de pilha (posso fornecer um rastreamento de pilha completo, se solicitado).

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 executar isso até a depuração algumas vezes - mesmo que a classe seja chamada e carregada, e pareça que o cliente é encontrado através do ControllerDispatcher -> Client :: find (id) e encontrado no ClientRepository, quando chega a PersonalAccessTokenFactory, o $client passed in is null (which explains why the $client-> id não pode ser encontrado, embora eu não tenha ideia de por que $ client é nulo neste ponto).

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

Coisas que fiz / tentei com alguma orientação da documentação e outros posts:

  • Criei manualmente um usuário no Tinker e crie o token por meio do Tinker - isso funciona .
  • Certifique-se de que o usuário esteja conectado antes de tentar gerar o token.
  • passaporte: instalar (e adicionar a opção --force)
  • Cliente de acesso pessoal garantido é gerado com passaporte: cliente --pessoal
  • Certifique-se de que AuthServiceProvider :: boot () contém o ClientID e o segredo do cliente (no .env).
  • migrar: atualizar seguido de passaporte: instalar --force
  • Remoção completa do Passport, removendo todos os arquivos, chaves, migrações e entradas de banco de dados, seguido por uma migração: atualização e reinstalação do Passport, junto com a geração de um cliente de acesso pessoal adicional (mesmo que um seja gerado durante o passaporte: instalação).

Não tenho certeza de onde procurar / o que mais tentar neste momento, então qualquer ajuda ou orientação seria muito apreciada!

Respostas

grantley Aug 21 2020 at 22:41

Acabei descobrindo a solução. O problema é multicamadas, em parte tendo a ver com a documentação desatualizada do Laravel em relação aos testes e Passport Personal Access Clients.

A primeira parte do problema tinha a ver com o uso do traço RefreshDatabase em meu teste de unidade. Visto que isso cria um banco de dados fictício com conjuntos de dados vazios, embora os próprios clientes existam no banco de dados real e no arquivo .env, quando o teste é executado, o teste não vê esses clientes como existentes no banco de dados fictício. Para resolver este problema, você deve criar um cliente na função de configuração antes que o teste seja executado.

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

Isso resolve o problema de ter um cliente nulo durante o teste, mas a partir do Laravel 7, o Laravel adicionou um requisito para os Clientes de Acesso Pessoal que o id e o segredo do cliente devem ser mantidos dentro do arquivo .env. Ao executar o teste, o teste verá o ID do cliente real e o segredo no .env, e falhará em validá-los com o cliente que foi criado e armazenado no banco de dados simulado, retornando outra exceção: "Falha na autenticação do cliente".

A solução para esse problema é criar um arquivo .env.testing no diretório principal do projeto, copiando o conteúdo do arquivo .env para ele e garantindo que as chaves abaixo existam com valores para o seu cliente de acesso pessoal principal criado ou copiando o segredo de um cliente gerado apenas para teste (eu aconselharia o último).

 PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1

 PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

Em seguida, usando o código abaixo, certifique-se de que o valor $ clientSecret seja o mesmo que o valor da chave em seu arquivo .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();
}

Isso criará um novo cliente, definirá o segredo do atributo para o valor na variável e atualizará o segredo do banco de dados fictício para conter o mesmo valor. Espero que isso ajude alguém com o mesmo problema.