I am designing a custom authentication scheme (based on public keys) alongside a stateless API, and decided Passport would fulfill the need for post-authentication requests.
Assuming the authentication succeeds, and the user is authenticated, they would receive a Personal Access Token, and use the token for all further requests. The trouble I'm experiencing (still after much searching through various forums and Stack Overflow) is that when using Laravel's built in testing suite, on the createToken() method, it generates an (admittedly common) exception:
"ErrorException : Trying to get property 'id' of non-object".
I am able to manually create a user through Tinker, and create a token through Tinker. However I'm experiencing problems when attempting to automate this process after authenticating.
Here is the relevant code snippet post-authentication:
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);
I've manually called Auth::login on the user, to ensure the user is logged in, and Auth::user() returns the user (not null). Upon executing the third line of code, the exception is thrown with the following mini stack-trace (I can provide a full stack-trace if requested).
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
From running this through debug a few times- even though the class is called and loaded, and it appears the Client is found through ControllerDispatcher -> Client::find(id) and found in ClientRepository, when it gets to PersonalAccessTokenFactory, the $client passed in is null (which explains why the $client->id can't be found, though I have no idea why the $client is null at this point).
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,
...
}
Things I have done/tried with some guidance from the documentation and other posts:
I'm not sure where else to look/what else to try at this point, so any help or guidance would be much appreciated!
I eventually discovered the solution. The problem is multi-layered, in part having to do with outdated Laravel documentation in regards to testing and Passport Personal Access Clients.
The first part of the problem had to do with using the trait RefreshDatabase on my unit test. Since this creates a mock database with empty datasets, although the clients themselves exist in the real database and the .env file, when the test is run, the test does not see those clients as existing in the mock database. To solve this problem, you must create a client in the setup function before the test is run.
public function setUp() : void
{
parent::setUp();
$this->createClient(); //Private method->Full code below
}
This solves the issue about having a null client during testing, but starting in Laravel 7, Laravel added a requirement for Personal Access Clients that the id and the client secret has to be kept inside the .env file. When running the test, the test will see the actual client id and secret in the .env, and fail to validate these with the client that was created and stored in the mock database, returning another exception: "Client Authentication Failed".
The solution to this problem is to create a .env.testing file in your main project directory, copying your .env file contents to it and ensuring that the keys below exist with values for either your main created Personal Access Client, or copying the secret from a client generated just for testing (I would advise the latter).
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value
Then using the code below, make sure the $clientSecret value is the same as the key value in your .env.testing file.
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();
}
This will create a new client, set the attribute secret to the value in the variable and update the mock database secret to contain the same value. Hopefully this helps anyone with the same issue.