Implement Custom autoloader and JwtDecoder
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Dabestaniha\AuthenticateBridge;
|
||||
|
||||
final class Autoloader
|
||||
{
|
||||
private array $prefixes;
|
||||
|
||||
public static function register(array $prefixes): void
|
||||
{
|
||||
$loader = new self($prefixes);
|
||||
|
||||
spl_autoload_register([$loader, 'load']);
|
||||
}
|
||||
|
||||
public function __construct(array $prefixes)
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
$prefix = rtrim($prefix, '\\').'\\';
|
||||
$normalized[$prefix] = rtrim($baseDir, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
uksort($normalized, fn (string $a, string $b) => strlen($b) <=> strlen($a));
|
||||
|
||||
$this->prefixes = $normalized;
|
||||
}
|
||||
|
||||
public function load(string $class): void
|
||||
{
|
||||
foreach ($this->prefixes as $prefix => $baseDir) {
|
||||
if (strncmp($class, $prefix, strlen($prefix)) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativeClass = substr($class, strlen($prefix));
|
||||
$file = $baseDir.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass).'.php';
|
||||
|
||||
if (is_file($file)) {
|
||||
require_once $file;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace Dabestaniha\AuthenticateBridge\App\Http\Controllers;
|
||||
|
||||
use Dabestaniha\AuthenticateBridge\App\Http\Requests\MahakAuthenticateRequest;
|
||||
use ReflectionMethod;
|
||||
use Firebase\JWT;
|
||||
use Dabestaniha\AuthenticateBridge\App\Support\JwtDecoder;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class AuthenticateController extends InvocableController
|
||||
{
|
||||
@@ -32,9 +32,15 @@ class AuthenticateController extends InvocableController
|
||||
$jwt_secret = get_configured_option('jwt-secret');
|
||||
|
||||
try {
|
||||
$decoded = (array) JWT\JWT::decode($token, new JWT\Key($jwt_secret, 'HS256'));
|
||||
return (array) $decoded['data'];
|
||||
} catch (\Exception $e) {
|
||||
$decoded = JwtDecoder::decodeHs256($token, $jwt_secret);
|
||||
$data = $decoded['data'] ?? null;
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new UnexpectedValueException('JWT data must be an object.');
|
||||
}
|
||||
|
||||
return $data;
|
||||
} catch (\Throwable $e) {
|
||||
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]);
|
||||
}
|
||||
}
|
||||
@@ -75,4 +81,4 @@ class AuthenticateController extends InvocableController
|
||||
do_action('wp_login', get_userdata($user_id)->user_login, get_userdata($user_id));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ abstract class BaseFormRequest
|
||||
public function __construct()
|
||||
{
|
||||
$this->url = parse_url($_SERVER['REQUEST_URI']);
|
||||
$this->form = [
|
||||
...$_GET,
|
||||
...$_POST,
|
||||
];
|
||||
$this->form = array_merge($_GET, $_POST);
|
||||
}
|
||||
|
||||
public function isRequestForThisRoute(): bool
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Dabestaniha\AuthenticateBridge\App\Support;
|
||||
|
||||
use JsonException;
|
||||
use stdClass;
|
||||
use UnexpectedValueException;
|
||||
|
||||
final class JwtDecoder
|
||||
{
|
||||
public static function decodeHs256(string $token, string $secret): array
|
||||
{
|
||||
if ($secret === '') {
|
||||
throw new UnexpectedValueException('JWT secret must not be empty.');
|
||||
}
|
||||
|
||||
$segments = explode('.', $token);
|
||||
|
||||
if (count($segments) !== 3) {
|
||||
throw new UnexpectedValueException('JWT must contain header, payload, and signature.');
|
||||
}
|
||||
|
||||
[$encodedHeader, $encodedPayload, $encodedSignature] = $segments;
|
||||
|
||||
if ($encodedHeader === '' || $encodedPayload === '' || $encodedSignature === '') {
|
||||
throw new UnexpectedValueException('JWT segments must not be empty.');
|
||||
}
|
||||
|
||||
$header = self::decodeJsonObject(self::base64UrlDecode($encodedHeader), 'JWT header');
|
||||
$payload = self::decodeJsonObject(self::base64UrlDecode($encodedPayload), 'JWT payload');
|
||||
|
||||
if (($header['alg'] ?? null) !== 'HS256') {
|
||||
throw new UnexpectedValueException('JWT algorithm must be HS256.');
|
||||
}
|
||||
|
||||
$signature = self::base64UrlDecode($encodedSignature);
|
||||
$expectedSignature = hash_hmac('sha256', $encodedHeader.'.'.$encodedPayload, $secret, true);
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
throw new UnexpectedValueException('JWT signature is invalid.');
|
||||
}
|
||||
|
||||
self::validateTimestamps($payload);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private static function base64UrlDecode(string $value): string
|
||||
{
|
||||
$remainder = strlen($value) % 4;
|
||||
|
||||
if ($remainder === 1) {
|
||||
throw new UnexpectedValueException('JWT base64 encoding is invalid.');
|
||||
}
|
||||
|
||||
if ($remainder > 0) {
|
||||
$value .= str_repeat('=', 4 - $remainder);
|
||||
}
|
||||
|
||||
$decoded = base64_decode(strtr($value, '-_', '+/'), true);
|
||||
|
||||
if ($decoded === false) {
|
||||
throw new UnexpectedValueException('JWT base64 encoding is invalid.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private static function decodeJsonObject(string $json, string $section): array
|
||||
{
|
||||
try {
|
||||
$decoded = json_decode($json, false, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $exception) {
|
||||
throw new UnexpectedValueException("$section is not valid JSON.", 0, $exception);
|
||||
}
|
||||
|
||||
if (!$decoded instanceof stdClass) {
|
||||
throw new UnexpectedValueException("$section must be a JSON object.");
|
||||
}
|
||||
|
||||
return self::objectToArray($decoded);
|
||||
}
|
||||
|
||||
private static function objectToArray(mixed $value): mixed
|
||||
{
|
||||
if ($value instanceof stdClass) {
|
||||
$items = [];
|
||||
|
||||
foreach (get_object_vars($value) as $key => $item) {
|
||||
$items[$key] = self::objectToArray($item);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $key => $item) {
|
||||
$value[$key] = self::objectToArray($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function validateTimestamps(array $payload): void
|
||||
{
|
||||
$now = time();
|
||||
|
||||
if (isset($payload['nbf']) && self::numericDate($payload['nbf'], 'nbf') > $now) {
|
||||
throw new UnexpectedValueException('JWT cannot be used before nbf.');
|
||||
}
|
||||
|
||||
if (isset($payload['iat']) && self::numericDate($payload['iat'], 'iat') > $now) {
|
||||
throw new UnexpectedValueException('JWT cannot be used before iat.');
|
||||
}
|
||||
|
||||
if (isset($payload['exp']) && self::numericDate($payload['exp'], 'exp') <= $now) {
|
||||
throw new UnexpectedValueException('JWT has expired.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function numericDate(mixed $value, string $claim): int
|
||||
{
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
throw new UnexpectedValueException("JWT $claim claim must be numeric.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user