You've already forked Php-Template
feat: implement Guzzle facade and update JwtMiddleware to use it
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m11s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m22s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m23s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m41s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m26s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m12s
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 4m11s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 4m22s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 4m23s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 4m41s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 4m26s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 3m12s
This commit is contained in:
@@ -4,9 +4,9 @@ echo "Installing xDebug"
|
||||
|
||||
apk add make gcc linux-headers autoconf alpine-sdk
|
||||
|
||||
curl -sL https://github.com/xdebug/xdebug/archive/3.4.0.tar.gz -o 3.4.0.tar.gz
|
||||
tar -xvf 3.4.0.tar.gz
|
||||
cd xdebug-3.4.0 || exit
|
||||
curl -sL https://github.com/xdebug/xdebug/archive/3.5.0alpha3.tar.gz -o 3.5.0alpha3.tar.gz
|
||||
tar -xvf 3.5.0alpha3.tar.gz
|
||||
cd xdebug-3.5.0alpha3 || exit
|
||||
phpize
|
||||
./configure --enable-xdebug
|
||||
make
|
||||
@@ -20,5 +20,5 @@ xdebug.client_host = host.docker.internal
|
||||
" > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||
|
||||
cd ..
|
||||
rm -rf xdebug-3.4.0
|
||||
rm -rf 3.4.0.tar.gz
|
||||
rm -rf xdebug-3.5.0alpha3
|
||||
rm -rf 3.5.0alpha3.tar.gz
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Siteworxpro\App\Http\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\WrapperClock;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Lcobucci\JWT\JwtFacade;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
|
||||
@@ -28,6 +27,7 @@ use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Controllers\Controller;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
@@ -133,21 +133,21 @@ class JwtMiddleware extends Middleware
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||
'message' => 'Unauthorized: Invalid token',
|
||||
'errors' => $violations
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
} catch (InvalidTokenStructure) {
|
||||
// Token could not be parsed due to malformed structure.
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'status_code' => CodesEnum::UNAUTHORIZED->value,
|
||||
'message' => 'Unauthorized: Invalid token',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
} catch (GuzzleException) {
|
||||
} catch (GuzzleException | \RuntimeException) {
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 501,
|
||||
'message' => 'Token validation service unavailable',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
'status_code' => CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => 'Token validation service unavailable or unknown error',
|
||||
], CodesEnum::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Expose all token claims as request attributes for downstream consumers.
|
||||
@@ -170,7 +170,6 @@ class JwtMiddleware extends Middleware
|
||||
* @return SignedWith Signature constraint used during JWT parsing.
|
||||
*
|
||||
* @throws \RuntimeException When no signing key is configured.
|
||||
* @throws GuzzleException On JWKS key retrieval issues.
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function getSignedWith(string $token): SignedWith
|
||||
@@ -205,9 +204,6 @@ class JwtMiddleware extends Middleware
|
||||
return new SignedWith(new Hmac256(), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
private function getJwksKey(string $url, string $keyId): Key
|
||||
{
|
||||
$cached = Redis::get('jwks_key_' . $keyId);
|
||||
@@ -215,15 +211,14 @@ class JwtMiddleware extends Middleware
|
||||
return InMemory::plainText($cached);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
$openIdConfig = $client->get($url);
|
||||
$openIdConfig = Guzzle::get($url);
|
||||
$body = json_decode($openIdConfig->getBody()->getContents(), true, JSON_THROW_ON_ERROR);
|
||||
$jwksUri = $body['jwks_uri'] ?? '';
|
||||
if (empty($jwksUri)) {
|
||||
throw new \RuntimeException('JWKS URI not found in OpenID configuration.');
|
||||
}
|
||||
|
||||
$jwksResponse = $client->get($jwksUri);
|
||||
$jwksResponse = Guzzle::get($jwksUri);
|
||||
$jwksBody = json_decode(
|
||||
$jwksResponse->getBody()->getContents(),
|
||||
true,
|
||||
@@ -234,7 +229,7 @@ class JwtMiddleware extends Middleware
|
||||
$firstKey = array_filter(
|
||||
$jwksBody['keys'],
|
||||
fn($key) => $key['kid'] === $keyId
|
||||
)[0] ?? null;
|
||||
)[0] ?? $jwksBody['keys'][0] ?? null;
|
||||
|
||||
if (empty($firstKey)) {
|
||||
throw new \RuntimeException('No matching key found in JWKS for key ID: ' . $keyId);
|
||||
|
||||
28
src/Services/Facades/Guzzle.php
Normal file
28
src/Services/Facades/Guzzle.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Services\Facades;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Siteworxpro\App\Services\Facade;
|
||||
|
||||
/**
|
||||
* @method static Response get(string $uri, array $options = [])
|
||||
* @method static Response post(string $uri, array $options = [])
|
||||
* @method static Response put(string $uri, array $options = [])
|
||||
* @method static Response delete(string $uri, array $options = [])
|
||||
* @method static Response patch(string $uri, array $options = [])
|
||||
* @method static Response head(string $uri, array $options = [])
|
||||
* @method static PromiseInterface sendAsync(\Psr\Http\Message\RequestInterface $request, array $options = [])
|
||||
* @method static PromiseInterface requestAsync(string $method, string $uri, array $options = [])
|
||||
*/
|
||||
class Guzzle extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return Client::class;
|
||||
}
|
||||
}
|
||||
21
tests/Facades/GuzzleTest.php
Normal file
21
tests/Facades/GuzzleTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\Tests\Facades;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
|
||||
class GuzzleTest extends AbstractFacade
|
||||
{
|
||||
protected function getFacadeClass(): string
|
||||
{
|
||||
return Guzzle::class;
|
||||
}
|
||||
|
||||
protected function getConcrete(): string
|
||||
{
|
||||
return Client::class;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,63 @@ use Nyholm\Psr7\ServerRequest;
|
||||
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
class JwtMiddlewareTest extends Middleware
|
||||
{
|
||||
private const string TEST_SIGNING_KEY = 'test_signing_key_123456444478901234';
|
||||
|
||||
private const string TEST_RSA_PRIVATE_KEY = <<<EOD
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAqTheAdlelxJL0K15BqUEo0lBzY06P7J0PhMfPlg2fgIJH+ng
|
||||
ZmrpYFhBkj2L5Fnvxz0y58eu9WhhokwpS0GzgFIw+KfLV/WLX4PgionsQshrt0Pi
|
||||
XvthaSH1xuYtg2N13dVVTv3Au0BBFLUHMrQ+bO5hgvowHBNfFf0GaHLW2m0eZ2Um
|
||||
hWbtdv4HxrXBO5gI2N4UevyQ+inczN7RBZR6ZzyNoDO6Up6kS23/58zOruO+PGi7
|
||||
q9eb7hU+getpVgA29wEWMgT+N6c5n5AcENgM1sHxZK43GR5vhMGbVJqnrUsMGof7
|
||||
rT9Lxey3gjPS2r5nz2PNFcQ1i07QKDzvQHp2wwIDAQABAoIBAFMAC9QaWzP8TGWJ
|
||||
gNBKhnDU0MrSl5yAmlWMKYn52JiLxQ/7Ng7mJ5wTDe5986zIlDyEfwCCyAUk8qaZ
|
||||
drOsATBSoCSGoM1+6aKq26r4JYNILNVSHal64XegqZ2qbu6ADWMGbXZ2Ll9qD8Hp
|
||||
XSN4lxn0/q0wrAJJWh094zO+CDZP+zBbX9oHxb5JAVxjCaNW84sI6/6agXM5zzgK
|
||||
wcBt5Y0i8V8f7n9kg+CPNqY6BKg7o2ONFYTEVKuuEnVS/eupHQwBWExPCdxc85Tb
|
||||
YqFL0dmgehE0OTQ6FrEN7Xh6jE4GMJtWmTvBNpqhsMZ0i08tAZSPs+Us9rnppKkK
|
||||
T1SC2xECgYEA7yOv4C7dtHmFbn0YfnbBEfgvGAubv5jPDtZ5u6tUEhhU3rOcWexM
|
||||
Xhj7OFV4I8lbu2t7GY+2BR7Y2ikOLW9MrOGo6qWhsjTQuZs6QaRKObcPvl2s0LYY
|
||||
GxD1u84VjHPzID2pKVPqxaQ7KdcIaujAedWwAf4PV/uK2prKdGvzIksCgYEAtSau
|
||||
4Ml1UpXvKxiBcVKsHIoEO0g3NL1+wAbdStg8TFi+leCMJoPwZ01t64BTtHF+pgDP
|
||||
vn6VEgDSP3J4+W3dVhoajQeKBioT3MpDRP/qKDsImi2zJrg+hh9DMTlZd0Ab3EXv
|
||||
ycjw3FWRcpcU/1l261fA/m3QPwZikF2VlO/0cmkCgYEAvtefCuy718RHHObOPlZt
|
||||
O/bxNmJFOEEttOyql39iB1LNoDB8bTLruwh6q/lheEXAZDChO8P5gdqdOnUbMF0r
|
||||
Nqib0i6+fOYzUHw1oJ8I8UhLUyOUv7ciQ69kPC15+u2psCglMKscp/+pi3lk6VS4
|
||||
DkLfRKfI/PDsXgq72O8xSEMCgYEApukSnvngyQxvR1UYB7N19AHTLlA21bh4LjTk
|
||||
905QGMR4Lp6sY9yTyIsWabRe69bbK9d5kvsNHX52OpGeF6z8EJaSujklGtLwZDJV
|
||||
UyE9vn3OSkkrVdTTfz8U6Sj/XxpJ0Wb7LwCftVR+ZIgCh9kF8ohzwbqq8zdN39jq
|
||||
t0V1BWkCgYEA2Mk2gOdYAN8aZgydFYKhogY5UNK/CFpq7hhekEyt73uxzxguVpZn
|
||||
AJ9mq2L1CVJ5WqAUk2IzioeR7XAndntesbOafDuR4mhCUJhX+m/YQlKbTrs2dScR
|
||||
S88z05AnmQmr5eCbQmVULZGo9xeLDB+GDWvvjpQ+NWcha2uO0O0RTQY=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
EOD;
|
||||
|
||||
private const string TEST_JWKS_JSON = <<<EOD
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"alg": "RS256",
|
||||
"e": "AQAB",
|
||||
"ext": true,
|
||||
"key_ops": [
|
||||
"verify"
|
||||
],
|
||||
"kty": "RSA",
|
||||
"n": "qTheAdlelxJL0K15BqUEo0lBzY06P7J0PhMfPlg2fgIJH-ngZmrpYFhBkj2L5Fnvxz0y58eu9WhhokwpS0GzgFIw-KfLV_WLX4PgionsQshrt0PiXvthaSH1xuYtg2N13dVVTv3Au0BBFLUHMrQ-bO5hgvowHBNfFf0GaHLW2m0eZ2UmhWbtdv4HxrXBO5gI2N4UevyQ-inczN7RBZR6ZzyNoDO6Up6kS23_58zOruO-PGi7q9eb7hU-getpVgA29wEWMgT-N6c5n5AcENgM1sHxZK43GR5vhMGbVJqnrUsMGof7rT9Lxey3gjPS2r5nz2PNFcQ1i07QKDzvQHp2ww",
|
||||
"kid": "2o5IaHnjxYtkpNWEcdPlwnaRJnaCJ2k2LY2nR4z6cN4=",
|
||||
"use": "sig"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOD;
|
||||
|
||||
public function getClass(): object
|
||||
{
|
||||
return new class {
|
||||
@@ -51,7 +102,7 @@ class JwtMiddlewareTest extends Middleware
|
||||
$class = new class {
|
||||
public function getCallable(): array
|
||||
{
|
||||
return [ $this, 'index' ];
|
||||
return [$this, 'index'];
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -208,6 +259,91 @@ class JwtMiddlewareTest extends Middleware
|
||||
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function testJwtFromJwkEndpoint()
|
||||
{
|
||||
Config::set('jwt.audience', 'https://client-app.io');
|
||||
Config::set('jwt.issuer', 'https://api.my-awesome-app.io');
|
||||
|
||||
Redis::partialMock()->shouldReceive('get')->andReturn(null);
|
||||
Redis::shouldReceive('set')->andReturn('OK');
|
||||
Guzzle::partialMock()->shouldReceive('get')
|
||||
->with('https://test.com/.well-known/openid-configuration')
|
||||
->andReturn(new Response(200, [], json_encode([
|
||||
'jwks_uri' => 'https://test.com/keys'
|
||||
], JSON_THROW_ON_ERROR)));
|
||||
|
||||
Guzzle::shouldReceive('get')
|
||||
->with('https://test.com/keys')
|
||||
->andReturn(new Response(200, [], self::TEST_JWKS_JSON));
|
||||
|
||||
Config::set('jwt.signing_key', 'https://test.com/.well-known/openid-configuration');
|
||||
|
||||
$class = $this->getClass();
|
||||
|
||||
$handler = \Mockery::mock(Dispatcher::class);
|
||||
$handler->shouldReceive('getMiddlewareStack')
|
||||
->andReturn([$class]);
|
||||
$handler
|
||||
->shouldReceive('handle')
|
||||
->once()
|
||||
->andReturn(new Response(200));
|
||||
|
||||
$request = new ServerRequest('GET', '/');
|
||||
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwtRsa());
|
||||
$middleware = new JwtMiddleware();
|
||||
$response = $middleware->process($request, $handler);
|
||||
$this->assertEquals(CodesEnum::OK->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function testCatchesInvalidJwksUrl()
|
||||
{
|
||||
Config::set('jwt.signing_key', 'https://test.com/.well-known/openid-configuration');
|
||||
Redis::partialMock()->shouldReceive('get')->andReturn(null);
|
||||
Redis::shouldReceive('set')->andReturn('OK');
|
||||
Guzzle::partialMock()->shouldReceive('get')
|
||||
->with('https://test.com/.well-known/openid-configuration')
|
||||
->andReturn(new Response(200, [], json_encode([], JSON_THROW_ON_ERROR)));
|
||||
|
||||
|
||||
|
||||
$class = $this->getClass();
|
||||
|
||||
$handler = \Mockery::mock(Dispatcher::class);
|
||||
$handler->shouldReceive('getMiddlewareStack')
|
||||
->andReturn([$class]);
|
||||
|
||||
$request = new ServerRequest('GET', '/');
|
||||
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getJwtRsa());
|
||||
$middleware = new JwtMiddleware();
|
||||
$response = $middleware->process($request, $handler);
|
||||
$this->assertEquals(CodesEnum::INTERNAL_SERVER_ERROR->value, $response->getStatusCode());
|
||||
}
|
||||
|
||||
private function getJwtRsa(): string
|
||||
{
|
||||
$key = InMemory::plainText(self::TEST_RSA_PRIVATE_KEY);
|
||||
$signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
|
||||
$token = new JwtFacade()->issue(
|
||||
$signer,
|
||||
$key,
|
||||
static fn(
|
||||
Builder $builder,
|
||||
DateTimeImmutable $issuedAt
|
||||
): Builder => $builder
|
||||
->issuedBy('https://api.my-awesome-app.io')
|
||||
->permittedFor('https://client-app.io')
|
||||
->expiresAt($issuedAt->modify('+10 minutes'))
|
||||
);
|
||||
|
||||
return $token->toString();
|
||||
}
|
||||
|
||||
private function getJwt(): string
|
||||
{
|
||||
$key = InMemory::plainText(self::TEST_SIGNING_KEY);
|
||||
@@ -216,7 +352,7 @@ class JwtMiddlewareTest extends Middleware
|
||||
$token = new JwtFacade()->issue(
|
||||
$signer,
|
||||
$key,
|
||||
static fn (
|
||||
static fn(
|
||||
Builder $builder,
|
||||
DateTimeImmutable $issuedAt
|
||||
): Builder => $builder
|
||||
|
||||
Reference in New Issue
Block a user