Skip to content

Commit

Permalink
Acting as a Keycloak user in tests (#103)
Browse files Browse the repository at this point in the history
* feat: acting as a Keycloak user in tests

* fix: test coverage and functionality logic changed a bit
  • Loading branch information
tuytoosh committed Mar 11, 2024
1 parent cc051f5 commit aa71dbe
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 17 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions src/ActingAsKeycloakUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace KeycloakGuard;

use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Config;

trait ActingAsKeycloakUser
{
public function actingAsKeycloakUser($user = null)
{
if (!$user) {
Config::set('keycloak.load_user_from_database', false);
}

$token = $this->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;
}
}
15 changes: 15 additions & 0 deletions src/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
20 changes: 20 additions & 0 deletions tests/AuthenticateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,8 @@

class AuthenticateTest extends TestCase
{
use ActingAsKeycloakUser;

protected function setUp(): void
{
parent::setUp();
Expand Down Expand Up @@ -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());
}

}
25 changes: 8 additions & 17 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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;
}
}

0 comments on commit aa71dbe

Please sign in to comment.