Could not load a KeycloakUser without an AccessToken
Maybe there is still some misconfiguration in my project but I am seeing these two errors.
When the user logs out from Keycloak (or their session is killed), a subsequent access to a page of the webapp returns one of these errors:
Could not load a KeycloakUser without an AccessToken. <-- this happens if the access token is expired
Call to a member function toArray() on null <-- this happens if the access token is NOT yet expired
Both are originated in the function loadUserByIdentifier($identifier) in KeycloakUserProvider.php:
public function loadUserByIdentifier($identifier): UserInterface
{
if (!$identifier instanceof AccessTokenInterface) { // <-- First error happens here
//throw new \LogicException('Could not load a KeycloakUser without an AccessToken.');
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
try {
$resourceOwner = $this->iamClient->fetchUserFromToken($identifier);
if ($resourceOwner === null) {
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
}
catch (\UnexpectedValueException $e) {
$this->keycloakClientLogger->warning($e->getMessage());
$this->keycloakClientLogger->warning('User should have been disconnected from Keycloak server');
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
$this->keycloakClientLogger->info('KeycloakUserProvider::loadUserByIdentifier', [
'resourceOwner' => $resourceOwner->toArray(), // <-- Second error happens here
]);
return $resourceOwner;
}
Well, the first is not actually an error, just an exception thrown in that particular situation. I need to keep the webapp running in this case, so I just throw the UserNotFoundException instead of LogicException to start a new login on Keicloak.
The second error is more subtle, because it's due to a call to $resourceOwner->toArray() with $resourceOwner=NULL:
try {
$resourceOwner = $this->iamClient->fetchUserFromToken($identifier);
if ($resourceOwner === null) {
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
}
catch (\UnexpectedValueException $e) { // <-- this block is never entered as the exception was catched in the calling function
$this->keycloakClientLogger->warning($e->getMessage());
$this->keycloakClientLogger->warning('User should have been disconnected from Keycloak server');
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
Here the catch block (\UnexpectedValueException $e) is never entered: the UnexpectedValueException is catched in the calling function fetchUserFromToken(AccessTokenInterface $token) on line 171 in KeycloakClient.php:
public function fetchUserFromToken(AccessTokenInterface $token): ?KeycloakResourceOwner
{
try {
$accessToken = new AccessTokenLib([
'access_token' => $token->getToken(),
'refresh_token' => $token->getRefreshToken(),
'expires' => $token->getExpires(),
'values' => $token->getValues(),
]);
$resourceOwner = $this->keycloakProvider->getResourceOwner($accessToken); // <-- throws UnexpectedValueException
$user = new KeycloakResourceOwner($resourceOwner->toArray(), $token);
$this->keycloakClientLogger->info('KeycloakClient::fetchUserFromToken', [
'user' => $user->toArray(),
]);
return $user;
}
catch (\Exception $e) { // <-- catched here
$this->keycloakClientLogger->error('KeycloakClient::fetchUserFromToken', [
'error' => $e->getMessage(),
]);
return null;
}
}
By catching the UnexpectedValueException with the generic catch (\Exception $e), a NULL KeycloakResourceOwner is returned to loadUserByIdentifier($identifier) without any exception to be catched, so it tries to execute this code:
$this->keycloakClientLogger->info('KeycloakUserProvider::loadUserByIdentifier', [
'resourceOwner' => $resourceOwner->toArray(), // <-- Second error happens here
]);
on a NULL $resourceOwner and the new exceptin "Call to a member function toArray() on null" is thrown.
The original UnexpectedValueException is thrown in vendor/league/oauth2-client/src/Provider/AbstractProvider.php at line 885:
protected function fetchResourceOwnerDetails(AccessToken $token)
{
$url = $this->getResourceOwnerDetailsUrl($token);
$request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token);
$response = $this->getParsedResponse($request);
if (false === is_array($response)) {
throw new UnexpectedValueException(
'Invalid response received from Authorization Server. Expected JSON.'
);
}
return $response;
The function fetchResourceOwnerDetails(AccessToken $token) is called by vendor/stevenmaguire/oauth2-keycloak/src/Provider/Keycloak.php at line 271:
public function getResourceOwner(AccessToken $token)
{
$response = $this->fetchResourceOwnerDetails($token);
// We are always getting an array. We have to check if it is
// the array we created
if (array_key_exists('jwt', $response)) {
$response = $response['jwt'];
}
$response = $this->decryptResponse($response);
return $this->createResourceOwner($response, $token);
}
but there's no exception catch here so it's passed back to KeycloakClient.php
As a quick fix I added this check:
if ($resourceOwner === null) {
throw new UserNotFoundException(sprintf('User with access token "%s" not found.', $identifier));
}
but you could probably find a better solution. Hope the above helps.