robsontenorio / laravel-keycloak-guard

🔑 Simple Keycloak Guard for Laravel
MIT License
434 stars 141 forks source link

Tests against live Keycloak instance in Laravel app #92

Closed SolveSoul closed 1 year ago

SolveSoul commented 1 year ago

Hi,

I'm currently having issues trying to write tests for authentication against a live Keycloak environment. More specifically, the request launched from the test is not the same request as the instance in the KeycloakGuard.

This is the scenario:

The request ($app->request) is automatically set at the start of the test via the KeycloakServiceProvider

Auth::extend('keycloak', function (app, $name, array $config) {
   return new KeycloakGuard(Auth::createUserProvider($config['provider']), $app->request);
});

Then when running the test I get a valid access token from the Keycloak instance and set it in the PHPUnit test like this

public function testAuthenticateUsingLiveKeycloakInstance()
{
    $endpoint = '/api/v1/my-protected-endpoint';

    $token = $this->getKeycloakToken();

    $this->withToken($token)
        ->get($endpoint)
        ->assertSuccessful();
}

So far, so good. However, since the request is was only just created and $app->request was already set in the KeycloakGuard at the start of the test it means that the token is not set in the request used by the KeycloakGuard. We can see this when dump the request.

See this dump example in the Authenticate middleware class of Laravel:

protected function authenticate($request, array $guards)
{
    if (empty($guards)) {
        $guards = [null];
    }

    foreach ($guards as $guard) {
        dd($request->headers, $this->auth->guard($guard)); // <----- dump here
        if ($this->auth->guard($guard)->check()) {
            return $this->auth->shouldUse($guard);
        }
    }

    $this->unauthenticated($request, $guards);
}

Output (redacted a bit so the important parts are visible):

Symfony\Component\HttpFoundation\HeaderBag {#6511 // vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:65
  #headers: array:7 [
    "host" => array:1 [
      0 => "127.0.0.1:8000"
    ]
    "user-agent" => array:1 [
      0 => "Symfony"
    ]
    "accept" => array:1 [
      0 => "application/json"
    ]
    "accept-language" => array:1 [
      0 => "nl-be,nl;q=0.5"
    ]
    "accept-charset" => array:1 [
      0 => "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
    ]
    "authorization" => array:1 [
      0 => "Bearer <redacted>"
    ]
    "content-type" => array:1 [
      0 => "application/x-www-form-urlencoded"
    ]
  ]
  #cacheControl: []
}

KeycloakGuard\KeycloakGuard {#7013 // vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:65
  # [...]
    +headers: Symfony\Component\HttpFoundation\HeaderBag {#2132
      #headers: array:5 [
        "host" => array:1 [
          0 => "127.0.0.1:8000"
        ]
        "user-agent" => array:1 [
          0 => "Symfony"
        ]
        "accept" => array:1 [
          0 => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
        ]
        "accept-language" => array:1 [
          0 => "en-us,en;q=0.5"
        ]
        "accept-charset" => array:1 [
          0 => "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
        ]
      ]
      #cacheControl: []
    }
  # [...]
}

As you can see, the first request is from the test and correctly contains the bearer token in the Authorization header. However, the second request which was set in the KeycloakServiceProvider at the test start does not contain the token but is used to check if a user is authenticated or not.

Any ideas on how this could be fixed?

Thanks in advance for any input (and thanks for the creation of this awesome library!)

robsontenorio commented 1 year ago

I advise you to use mocks on tests. You should never hit real Keycloak instance.

SolveSoul commented 1 year ago

@robsontenorio yes, that's very solid feedback!

However, our application is currently migrating from using the native Laravel authentication using sessions to a Keycloak instance using tokens. So all existing unit tests would need to be mocked against every single possible API call of keycloak with all possible different outputs which is a time consuming task (at least in our case).

We've created an isolated test-specific realm which is used for testing and it seems to be working great so far.

Anyway, I've found a work-around:

If request() is used instead of $this->request in the KeycloakGuard class then the request object is always up-to-date instead of the one being set when Laravel is bootstrapped. Since #94 is merged it's now possible to have more control over the request property and the work-around can be implemented.

The last consideration for this issue could be to use request() instead of passing app->request into the constructor of the KeycloakGuard but at this point in time I'm not sure if this causes any side-effects.

SolveSoul commented 1 year ago

Here's how I fixed the authentication which works for testing against a live Keycloak instance and normal use

Custom guard which is made possible since PR #94

class ExtendedKeycloakGuard extends KeycloakGuard
{
    public function __construct(UserProvider $provider)
    {
        parent::__construct($provider, request());
    }

    public function user(): Authenticatable|null
    {
        if (request()->bearerToken()) {
            $this->authenticate();
        }

        return parent::user();
    }

    public function getTokenForRequest(): ?string
    {
        $this->request = request();

        return parent::getTokenForRequest();
    }
}

then in AppServiceProvider.php

Auth::extend('keycloak', function ($app, $name, array $config) {
    return new ExtendedKeycloakGuard(Auth::createUserProvider($config['provider']));
});