Skip to content

Commit

Permalink
Merge pull request #32 from mainick/enhance-jwt-introspection
Browse files Browse the repository at this point in the history
Enhance jwt introspection
  • Loading branch information
mainick authored Nov 19, 2024
2 parents ae84a02 + 4d1a119 commit 34af093
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 20 deletions.
20 changes: 20 additions & 0 deletions src/Exception/TokenDecoderException.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,24 @@ public function __construct(string $string, \Exception $e)
{
parent::__construct($string, self::CODE, $e);
}

public static function forSignatureValidationFailure(\Exception $e): self
{
return new self('Signature validation failed', $e);
}

public static function forExpiration(\Exception $e): self
{
return new self('Token has expired', $e);
}

public static function forIssuerMismatch(\Exception $e): self
{
return new self('Issuer mismatch', $e);
}

public static function forAudienceMismatch(\Exception $e): self
{
return new self('Audience mismatch', $e);
}
}
6 changes: 6 additions & 0 deletions src/Interface/TokenDecoderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ interface TokenDecoderInterface
* @return array<string, mixed>
*/
public function decode(string $token, string $key): array;

/**
* @param string $realm
* @param array<string, mixed> $tokenDecoded
*/
public function validateToken(string $realm, array $tokenDecoded): void;
}
1 change: 1 addition & 0 deletions src/Provider/KeycloakClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public function verifyToken(AccessTokenInterface $token): ?UserRepresentationDTO

$decoder = TokenDecoderFactory::create($this->encryption_algorithm);
$tokenDecoded = $decoder->decode($accessToken->getToken(), $this->encryption_key);
$decoder->validateToken($this->realm, $tokenDecoded);
$this->keycloakClientLogger->info('KeycloakClient::verifyToken', [
'tokenDecoded' => $tokenDecoded,
]);
Expand Down
29 changes: 23 additions & 6 deletions src/Token/HS256TokenDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,41 @@

namespace Mainick\KeycloakClientBundle\Token;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Mainick\KeycloakClientBundle\Exception\TokenDecoderException;
use Mainick\KeycloakClientBundle\Interface\TokenDecoderInterface;

class HS256TokenDecoder implements TokenDecoderInterface
{

public function decode(string $token, string $key): array
{
// https://github.com/firebase/php-jwt#example-encodedecode-headers
[$headersB64, $payloadB64, $sig] = explode('.', $token);
$tokenDecoded = json_decode(base64_decode($payloadB64), true, 512, JSON_THROW_ON_ERROR);

try {
$tokenDecoded = JWT::decode($token, new Key($key, 'HS256'));

$json = json_encode($tokenDecoded, JSON_THROW_ON_ERROR);

return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
}
catch (\Exception $e) {
} catch (\Exception $e) {
throw new TokenDecoderException('Error decoding token', $e);
}
}

public function validateToken(string $realm, array $tokenDecoded): void
{
$now = time();

if ($tokenDecoded['exp'] < $now) {
throw TokenDecoderException::forExpiration(new \Exception('Token has expired'));
}

if (str_contains($tokenDecoded['iss'], $realm) === false) {
throw TokenDecoderException::forIssuerMismatch(new \Exception('Invalid token issuer'));
}
//
// if ($tokenDecoded['aud'] !== 'account') {
// throw TokenDecoderException::forAudienceMismatch(new \Exception('Invalid token audience'));
// }
}
}
19 changes: 14 additions & 5 deletions src/Token/RS256TokenDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ class RS256TokenDecoder implements TokenDecoderInterface
{
public function decode(string $token, string $key): array
{
$publicKeyPem = <<<EOD
try {
$publicKeyPem = <<<EOD
-----BEGIN PUBLIC KEY-----
$key
-----END PUBLIC KEY-----
EOD;
$publicKey = openssl_get_publickey($publicKeyPem);
$publicKey = openssl_get_publickey($publicKeyPem);

$headers = new \stdClass();
$tokenDecoded = JWT::decode($token, new Key($publicKey, 'RS256'), $headers);
$headers = new \stdClass();
$tokenDecoded = JWT::decode($token, new Key($publicKey, 'RS256'), $headers);

try {
$json = json_encode($tokenDecoded, JSON_THROW_ON_ERROR);

return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
Expand All @@ -32,4 +32,13 @@ public function decode(string $token, string $key): array
throw new TokenDecoderException('Error decoding token', $e);
}
}

public function validateToken(string $realm, array $tokenDecoded): void
{
$now = time();

if ($tokenDecoded['exp'] < $now) {
throw TokenDecoderException::forExpiration(new \Exception('Token has expired'));
}
}
}
16 changes: 8 additions & 8 deletions src/Token/TokenDecoderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@

class TokenDecoderFactory
{
public const ALGORITHM_RS256 = 'RS256';
public const ALGORITHM_HS256 = 'HS256';

#[Pure]
public static function create($algorithm): TokenDecoderInterface
{
switch ($algorithm) {
case 'RS256':
return new RS256TokenDecoder();
case 'HS256':
return new HS256TokenDecoder();
default:
throw new \RuntimeException('Invalid algorithm');
}
return match ($algorithm) {
self::ALGORITHM_RS256 => new RS256TokenDecoder(),
self::ALGORITHM_HS256 => new HS256TokenDecoder(),
default => throw new \RuntimeException('Invalid algorithm'),
};
}
}
4 changes: 3 additions & 1 deletion tests/Provider/KeycloakClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class KeycloakClientTest extends TestCase
"exp": "%s",
"iat": "%s",
"jti": "e11a85c8-aa91-4f75-9088-57db4586f8b9",
"iss": "https://example.org/auth/realms/test-realm",
"iss": "https://example.org/auth/realms/mock_realm",
"aud": "account",
"nbf": "%s",
"sub": "4332085e-b944-4acc-9eb1-27d8f5405f3e",
Expand Down Expand Up @@ -93,6 +93,8 @@ protected function setUp(): void
'test-app',
'mock_secret',
'none',
self::ENCRYPTION_ALGORITHM,
self::ENCRYPTION_KEY
);

$jwt_tmp = sprintf($this->jwtTemplate, time() + 3600, time(), time());
Expand Down

0 comments on commit 34af093

Please sign in to comment.