KeycloakClientBundle icon indicating copy to clipboard operation
KeycloakClientBundle copied to clipboard

Could not load a KeycloakUser without an AccessToken

Open stefanomarty opened this issue 1 year ago • 0 comments

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.

stefanomarty avatar May 07 '24 13:05 stefanomarty