chore: added documentation (#16)
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m42s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m38s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m44s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m28s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m27s

Reviewed-on: #16
Co-authored-by: Ron Rise <ron@siteworxpro.com>
Co-committed-by: Ron Rise <ron@siteworxpro.com>
This commit was merged in pull request #16.
This commit is contained in:
2025-11-13 04:14:13 +00:00
committed by Siteworx Pro Gitea
parent e4a55af694
commit 5542ad1e75
28 changed files with 514 additions and 84 deletions

View File

@@ -27,18 +27,44 @@ use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Services\Facades\Config;
use Siteworxpro\HttpStatus\CodesEnum;
/**
* JWT authorization middleware.
*
* Applies JWT validation to controller actions annotated with `Jwt` attribute.
* Flow:
* - Resolve the targeted controller and method for the current route.
* - If the method has `Jwt`, read the `Authorization` header and parse the Bearer token.
* - Validate signature, time constraints, issuer\(\) and audience\(\) based on attribute and config.
* - On success, attach all token claims to the request as attributes.
* - On failure, return a 401 JSON response with validation errors.
*
* Configuration:
* - `jwt.signing_key`: key material or `file://` path to key.
* - `jwt.strict_validation`: bool toggling strict vs loose time validation.
*/
class JwtMiddleware extends Middleware
{
/**
* @throws \JsonException
* @throws \Exception
* Process the incoming request.
*
* If the matched controller method is annotated with `Jwt`, validates the token and
* augments the request with claims on success. Otherwise, just delegates to the next handler.
*
* @param ServerRequestInterface $request PSR-7 request instance.
* @param RequestHandlerInterface|Dispatcher $handler Next middleware or route dispatcher.
*
* @return ResponseInterface Response produced by the next handler or a 401 JSON response.
*
* @throws \JsonException On JSON error response encoding issues.
* @throws \Exception On unexpected reflection or JWT parsing issues.
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface|Dispatcher $handler
): ResponseInterface {
$callable = $this->extractRouteCallable($request, $handler);
// Resolve the callable \[Controller, method] for the current route.
$callable = $this->extractRouteCallable($handler);
if ($callable === null) {
return $handler->handle($request);
}
@@ -51,12 +77,15 @@ class JwtMiddleware extends Middleware
if ($reflectionClass->hasMethod($method)) {
$reflectionMethod = $reflectionClass->getMethod($method);
// Read `Jwt` attribute on the controller method.
$attributes = $reflectionMethod->getAttributes(Jwt::class);
// If no `Jwt` attribute, do not enforce auth here.
if (empty($attributes)) {
return $handler->handle($request);
}
// Extract Bearer token from Authorization header.
$token = str_replace('Bearer ', '', $request->getHeaderLine('Authorization'));
if (empty($token)) {
@@ -66,6 +95,7 @@ class JwtMiddleware extends Middleware
], CodesEnum::UNAUTHORIZED);
}
// Aggregate required issuers and audience from attributes.
$requiredIssuers = [];
$requiredAudience = '';
@@ -81,6 +111,7 @@ class JwtMiddleware extends Middleware
}
try {
// Parse and validate the token with signature, time, issuer and audience constraints.
$jwt = new JwtFacade()->parse(
$token,
$this->getSignedWith(),
@@ -91,6 +122,7 @@ class JwtMiddleware extends Middleware
new PermittedFor($requiredAudience)
);
} catch (RequiredConstraintsViolated $exception) {
// Collect human-readable violations to return to the client.
$violations = [];
foreach ($exception->violations() as $violation) {
$violations[] = $violation->getMessage();
@@ -102,12 +134,14 @@ class JwtMiddleware extends Middleware
'errors' => $violations
], CodesEnum::UNAUTHORIZED);
} catch (InvalidTokenStructure) {
// Token could not be parsed due to malformed structure.
return JsonResponseFactory::createJsonResponse([
'status_code' => 401,
'message' => 'Unauthorized: Invalid token',
], CodesEnum::UNAUTHORIZED);
}
// Expose all token claims as request attributes for downstream consumers.
foreach ($jwt->claims()->all() as $item => $value) {
$request = $request->withAttribute($item, $value);
}
@@ -117,6 +151,17 @@ class JwtMiddleware extends Middleware
return $handler->handle($request);
}
/**
* Build the signature validation constraint from configured key.
*
* - If the configured key content includes the string `PUBLIC KEY`, use RSA SHA-256.
* - Otherwise assume an HMAC SHA-256 shared secret.
* - Supports raw key strings or `file://` paths.
*
* @return SignedWith Signature constraint used during JWT parsing.
*
* @throws \RuntimeException When no signing key is configured.
*/
private function getSignedWith(): SignedWith
{
$key = Config::get('jwt.signing_key');
@@ -125,12 +170,14 @@ class JwtMiddleware extends Middleware
throw new \RuntimeException('JWT signing key is not configured.');
}
// Load key either from file or raw text.
if (str_starts_with($key, 'file://')) {
$key = InMemory::file(substr($key, 7));
} else {
$key = InMemory::plainText($key);
}
// Heuristic: if PEM public key content is detected, use RSA; otherwise use HMAC.
if (str_contains($key->contents(), 'PUBLIC KEY')) {
return new SignedWith(new Sha256(), $key);
}