From aa71dbea528dddaead6cd683be2b5090ca70679b Mon Sep 17 00:00:00 2001 From: Hamid Haghdoost <8298095+tuytoosh@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:52:38 +0100 Subject: [PATCH] Acting as a Keycloak user in tests (#103) * feat: acting as a Keycloak user in tests * fix: test coverage and functionality logic changed a bit --- README.md | 15 ++++++++++++ src/ActingAsKeycloakUser.php | 46 ++++++++++++++++++++++++++++++++++++ src/Token.php | 15 ++++++++++++ tests/AuthenticateTest.php | 20 ++++++++++++++++ tests/TestCase.php | 25 +++++++------------- 5 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 src/ActingAsKeycloakUser.php diff --git a/README.md b/README.md index 5794d08..3fbfb37 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,21 @@ Auth::hasAnyRole('myapp-frontend', ['myapp-frontend-role1', 'myapp-frontend-role Auth::hasAnyRole('myapp-backend', ['myapp-frontend-role1', 'myapp-frontend-role2']) // false ``` +# Acting as a Keycloak user in tests + +As an equivelant feature like `$this->actingAs($user)` in Laravel, with this package you can use `KeycloakGuard\ActingAsKeycloakUser` trait in your test class and then use `actingAsKeycloakUser()` method to act as a user and somehow skip the Keycloak auth: + +```php +use KeycloakGuard\ActingAsKeycloakUser; + +public test_a_protected_route() +{ + $this->actingAsKeycloakUser() + ->getJson('/api/somewhere') + ->assertOk(); +} +``` + #### Scope Example decoded payload: ```json diff --git a/src/ActingAsKeycloakUser.php b/src/ActingAsKeycloakUser.php new file mode 100644 index 0000000..3664073 --- /dev/null +++ b/src/ActingAsKeycloakUser.php @@ -0,0 +1,46 @@ +generateKeycloakToken($user); + + $this->withHeader('Authorization', 'Bearer '.$token); + + return $this; + } + + public function generateKeycloakToken($user = null) + { + $privateKey = openssl_pkey_new([ + 'digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA + ]); + + $publicKey = openssl_pkey_get_details($privateKey)['key']; + + $publicKey = Token::plainPublicKey($publicKey); + + Config::set('keycloak.realm_public_key', $publicKey); + + $payload = [ + 'preferred_username' => $user->username ?? config('keycloak.preferred_username'), + 'resource_access' => [config('keycloak.allowed_resources') => []] + ]; + + $token = JWT::encode($payload, $privateKey, 'RS256'); + + return $token; + } +} diff --git a/src/Token.php b/src/Token.php index 04eefc1..b6aa25a 100644 --- a/src/Token.php +++ b/src/Token.php @@ -32,4 +32,19 @@ private static function buildPublicKey(string $key) { return "-----BEGIN PUBLIC KEY-----\n".wordwrap($key, 64, "\n", true)."\n-----END PUBLIC KEY-----"; } + + /** + * Get the plain public key from a string + * + * @param string $key + * @return string + */ + public static function plainPublicKey(string $key): string + { + $string = str_replace('-----BEGIN PUBLIC KEY-----', '', $key); + $string = trim(str_replace('-----END PUBLIC KEY-----', '', $string)); + $string = str_replace('\n', '', $string); + + return $string; + } } diff --git a/tests/AuthenticateTest.php b/tests/AuthenticateTest.php index b88de50..032a795 100644 --- a/tests/AuthenticateTest.php +++ b/tests/AuthenticateTest.php @@ -5,6 +5,7 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Hashing\BcryptHasher; use Illuminate\Support\Facades\Auth; +use KeycloakGuard\ActingAsKeycloakUser; use KeycloakGuard\Exceptions\ResourceAccessNotAllowedException; use KeycloakGuard\Exceptions\TokenException; use KeycloakGuard\Exceptions\UserNotFoundException; @@ -14,6 +15,8 @@ class AuthenticateTest extends TestCase { + use ActingAsKeycloakUser; + protected function setUp(): void { parent::setUp(); @@ -405,4 +408,21 @@ public function test_authentication_prefers_bearer_token_over_with_custom_input_ $this->json('POST', '/foo/secret', ['api_token' => $this->token]); } + + public function test_with_keycloak_token_trait() + { + $this->actingAsKeycloakUser($this->user)->json('GET', '/foo/secret'); + + $this->assertEquals($this->user->username, Auth::user()->username); + } + + public function test_acting_as_keycloak_user_trait_without_user() + { + $this->actingAsKeycloakUser()->json('GET', '/foo/secret'); + + $this->assertTrue(Auth::hasUser()); + + $this->assertFalse(Auth::guest()); + } + } diff --git a/tests/TestCase.php b/tests/TestCase.php index 0fe568e..e67a833 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,6 +10,7 @@ use KeycloakGuard\KeycloakGuardServiceProvider; use KeycloakGuard\Tests\Factories\UserFactory; use KeycloakGuard\Tests\Models\User; +use KeycloakGuard\Token; use OpenSSLAsymmetricKey; use Orchestra\Testbench\TestCase as Orchestra; @@ -68,7 +69,7 @@ protected function defineEnvironment($app) ]); $app['config']->set('keycloak', [ - 'realm_public_key' => $this->plainPublicKey(), + 'realm_public_key' => Token::plainPublicKey($this->publicKey), 'user_provider_credential' => 'username', 'token_principal_attribute' => 'preferred_username', 'append_decoded_token' => false, @@ -94,17 +95,7 @@ protected function getPackageProviders($app) return [KeycloakGuardServiceProvider::class]; } - // Just extract a string from the public key, as required by config file - protected function plainPublicKey(): string - { - $string = str_replace('-----BEGIN PUBLIC KEY-----', '', $this->publicKey); - $string = trim(str_replace('-----END PUBLIC KEY-----', '', $string)); - $string = str_replace('\n', '', $string); - - return $string; - } - - // Build a diferent token with custom payload + // Build a different token with custom payload protected function buildCustomToken(array $payload) { $payload = array_replace($this->payload, $payload); @@ -113,10 +104,10 @@ protected function buildCustomToken(array $payload) } // Setup default token, for the default user - public function withKeycloakToken() - { - $this->withToken($this->token); + public function withKeycloakToken() + { + $this->withToken($this->token); - return $this; - } + return $this; + } }