From 322f484144a71fb77ecf1475a6d8ee47c3f91345 Mon Sep 17 00:00:00 2001 From: MeghdadFadaee Date: Mon, 24 Aug 2026 09:53:16 +0330 Subject: [PATCH] fix: add safe clock diagnostics to debug reports --- README.md | 2 +- dabestaniha-authenticate-bridge.php | 4 +- .../Controllers/AuthenticateController.php | 37 ++++- src/helpers.php | 138 +++++++++++++++++- src/resources/views/debug-error.php | 85 +++++++++-- test/integration-authentication.php | 40 ++++- 6 files changed, 282 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6e16fca..d1eb0f2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Authenticate WordPress user via json web token from Laravel Application. - Custom invalid token page - Optional debug mode with full exception details and stack traces -Debug mode is disabled by default. An administrator can enable it from **Settings > Mahak Authentication** while troubleshooting. Because diagnostic pages expose server paths and other technical details to visitors of the login URL, disable it again after diagnosing the problem. +Debug mode is disabled by default. An administrator can enable it from **Settings > Mahak Authentication** while troubleshooting. The debug page includes server clock/timezone data, safe JWT timing claims, and a Copy Markdown report. JWT contents, secrets, request parameters, and stack-trace arguments are omitted. Because diagnostic pages still expose server paths and other technical details to visitors of the login URL, disable it again after diagnosing the problem. ## Updates diff --git a/dabestaniha-authenticate-bridge.php b/dabestaniha-authenticate-bridge.php index 7c45649..fdaf433 100644 --- a/dabestaniha-authenticate-bridge.php +++ b/dabestaniha-authenticate-bridge.php @@ -4,7 +4,7 @@ * Plugin Name: Mahak Authenticate Bridge * Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Description: Authenticate WordPress user via json web token from Mahakiha Application. - * Version: 2.2.0 + * Version: 2.2.1 * Requires PHP: 7.4 * Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Author: Dabestaniha @@ -18,7 +18,7 @@ use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageMenuControll use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageFormController; use Dabestaniha\AuthenticateBridge\App\Support\PluginUpdater; -define('MAHAK_AUTHENTICATE_BRIDGE_VERSION', '2.2.0'); +define('MAHAK_AUTHENTICATE_BRIDGE_VERSION', '2.2.1'); define('MAHAK_AUTHENTICATE_BRIDGE_FILE', __FILE__); require_once __DIR__.'/src/Autoloader.php'; diff --git a/src/app/Http/Controllers/AuthenticateController.php b/src/app/Http/Controllers/AuthenticateController.php index c53d558..88b94ec 100644 --- a/src/app/Http/Controllers/AuthenticateController.php +++ b/src/app/Http/Controllers/AuthenticateController.php @@ -10,10 +10,16 @@ use UnexpectedValueException; class AuthenticateController extends InvocableController { + private array $debugContext = []; + public function __invoke(MahakAuthenticateRequest $request) { $token = $request->string('token'); + if (mahak_debug_enabled()) { + $this->debugContext['jwt_timestamps'] = $this->inspectJwtTimestamps($token); + } + $payload = $this->decrypt_jwt_token($token); $identifier_name = get_configured_option('user-identifier'); @@ -85,7 +91,36 @@ class AuthenticateController extends InvocableController $status = $exception->getCode() === 403 ? 403 : 500; $fallbackView = $exception instanceof UnexpectedValueException ? 'invalid-token' : 'server-error'; - mahak_render_error($exception, $status, $fallbackView); + mahak_render_error($exception, $status, $fallbackView, $this->debugContext); + } + + private function inspectJwtTimestamps(string $token): array + { + $segments = explode('.', $token); + + if (count($segments) !== 3) { + return []; + } + + $encodedPayload = strtr($segments[1], '-_', '+/'); + $encodedPayload .= str_repeat('=', (4 - strlen($encodedPayload) % 4) % 4); + $json = base64_decode($encodedPayload, true); + $payload = $json === false ? null : json_decode($json, true); + + if (!is_array($payload)) { + return []; + } + + $timestamps = []; + + foreach (['iat', 'nbf', 'exp'] as $claim) { + if (array_key_exists($claim, $payload)) { + $value = is_scalar($payload[$claim]) ? (string) $payload[$claim] : '[non-scalar]'; + $timestamps[$claim] = strlen($value) > 64 ? substr($value, 0, 64).'…' : $value; + } + } + + return $timestamps; } diff --git a/src/helpers.php b/src/helpers.php index d08b01c..14a96bc 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -20,11 +20,16 @@ if (!function_exists('mahak_debug_enabled')) { } if (!function_exists('mahak_render_error')) { - function mahak_render_error(\Throwable $exception, int $status = 500, string $fallbackView = 'server-error'): void + function mahak_render_error( + \Throwable $exception, + int $status = 500, + string $fallbackView = 'server-error', + array $context = [] + ): void { if (mahak_debug_enabled()) { wp_die( - view('debug-error', ['exception' => $exception]), + view('debug-error', ['report' => mahak_build_debug_report($exception, $context)]), 'Mahak Authentication Error', ['response' => $status] ); @@ -34,6 +39,135 @@ if (!function_exists('mahak_render_error')) { } } +if (!function_exists('mahak_build_debug_report')) { + function mahak_build_debug_report(\Throwable $exception, array $context = []): array + { + $now = time(); + $wordpressTimezone = function_exists('wp_timezone_string') + ? wp_timezone_string() + : (string) get_option('timezone_string', ''); + + if ($wordpressTimezone === '') { + $wordpressTimezone = 'UTC offset '.(string) get_option('gmt_offset', 0); + } + + $server = [ + 'Unix timestamp' => (string) $now, + 'UTC time' => gmdate('Y-m-d H:i:s \U\T\C', $now), + 'PHP local time' => date('Y-m-d H:i:s P T', $now), + 'PHP timezone' => date_default_timezone_get(), + 'PHP date.timezone' => (string) (ini_get('date.timezone') ?: '[not set]'), + 'WordPress timezone' => $wordpressTimezone, + 'WordPress version' => get_bloginfo('version'), + 'PHP version' => PHP_VERSION, + 'Plugin version' => defined('MAHAK_AUTHENTICATE_BRIDGE_VERSION') + ? MAHAK_AUTHENTICATE_BRIDGE_VERSION + : '[unknown]', + ]; + + $jwtTimestamps = []; + + foreach (($context['jwt_timestamps'] ?? []) as $claim => $value) { + $jwtTimestamps[strtoupper((string) $claim)] = mahak_format_jwt_timestamp((string) $value, $now); + } + + $trace = mahak_sanitized_exception_trace($exception); + $exceptionData = [ + 'Type' => get_class($exception), + 'Message' => $exception->getMessage(), + 'Location' => $exception->getFile().':'.$exception->getLine(), + ]; + + $markdown = "# Mahak Authentication Error\n\n## Exception\n\n"; + + foreach ($exceptionData as $label => $value) { + $markdown .= '- '.$label.': `'.mahak_markdown_value($value)."`\n"; + } + + $markdown .= "\n## WordPress server diagnostics\n\n| Field | Value |\n|---|---|\n"; + + foreach ($server as $label => $value) { + $markdown .= '| '.mahak_markdown_value($label).' | `'.mahak_markdown_value($value)."` |\n"; + } + + $markdown .= "\n## JWT timing claims\n\n"; + + if ($jwtTimestamps === []) { + $markdown .= "No readable JWT timing claims were available.\n"; + } else { + $markdown .= "| Claim | Value |\n|---|---|\n"; + + foreach ($jwtTimestamps as $label => $value) { + $markdown .= '| '.mahak_markdown_value($label).' | `'.mahak_markdown_value($value)."` |\n"; + } + } + + $markdown .= "\n## Sanitized stack trace\n\n```text\n".str_replace('```', "'''", $trace)."\n```\n"; + $markdown .= "\n> JWT contents, request parameters, and function arguments are intentionally omitted.\n"; + + return [ + 'exception' => $exceptionData, + 'server' => $server, + 'jwt_timestamps' => $jwtTimestamps, + 'trace' => $trace, + 'markdown' => $markdown, + ]; + } +} + +if (!function_exists('mahak_format_jwt_timestamp')) { + function mahak_format_jwt_timestamp(string $value, int $serverNow): string + { + if (!is_numeric($value)) { + return $value.' (not numeric)'; + } + + $numericValue = (float) $value; + $note = ''; + + if (is_infinite($numericValue) || is_nan($numericValue)) { + return $value.' (invalid numeric value)'; + } + + if (abs($numericValue) >= 100000000000) { + $numericValue /= 1000; + $note = '; appears to use milliseconds'; + } + + $timestamp = (int) $numericValue; + $difference = $timestamp - $serverNow; + $differenceLabel = ($difference >= 0 ? '+' : '').$difference.' seconds vs server'; + + return $value.' ('.gmdate('Y-m-d H:i:s \U\T\C', $timestamp).'; '.$differenceLabel.$note.')'; + } +} + +if (!function_exists('mahak_sanitized_exception_trace')) { + function mahak_sanitized_exception_trace(\Throwable $exception): string + { + $lines = []; + + foreach ($exception->getTrace() as $index => $frame) { + $location = isset($frame['file']) + ? $frame['file'].':'.($frame['line'] ?? '?') + : '[internal function]'; + $call = ($frame['class'] ?? '').($frame['type'] ?? '').($frame['function'] ?? '[unknown]').'()'; + $lines[] = '#'.$index.' '.$location.' '.$call; + } + + $lines[] = '#'.count($lines).' {main}'; + + return implode("\n", $lines); + } +} + +if (!function_exists('mahak_markdown_value')) { + function mahak_markdown_value(string $value): string + { + return str_replace(["\r", "\n", '|', '`'], [' ', ' ', '\\|', "'"], $value); + } +} + if (!function_exists('dd')) { function dd(): void { diff --git a/src/resources/views/debug-error.php b/src/resources/views/debug-error.php index 36d5275..b2caf96 100644 --- a/src/resources/views/debug-error.php +++ b/src/resources/views/debug-error.php @@ -8,24 +8,83 @@ body { margin: 2rem; color: #1d2327; background: #f0f0f1; font: 14px/1.5 monospace; } main { max-width: 1100px; margin: auto; padding: 2rem; background: #fff; border-left: 4px solid #d63638; box-shadow: 0 1px 3px rgba(0, 0, 0, .12); } h1 { margin-top: 0; color: #d63638; font: 24px/1.3 sans-serif; } - dt { margin-top: 1rem; font-weight: 700; } - dd { margin: .25rem 0 0; overflow-wrap: anywhere; } - pre { overflow: auto; padding: 1rem; color: #f0f0f1; background: #1d2327; white-space: pre-wrap; } + h2 { margin-top: 2rem; font: 20px/1.3 sans-serif; } + table { width: 100%; border-collapse: collapse; } + th, td { padding: .55rem; border: 1px solid #c3c4c7; text-align: left; vertical-align: top; overflow-wrap: anywhere; } + th { width: 220px; background: #f6f7f7; } + pre, textarea { box-sizing: border-box; width: 100%; padding: 1rem; color: #f0f0f1; background: #1d2327; white-space: pre-wrap; } + textarea { min-height: 240px; resize: vertical; } + button { padding: .55rem 1rem; border: 1px solid #2271b1; border-radius: 3px; color: #fff; background: #2271b1; cursor: pointer; } + #copy-status { margin-left: .75rem; font-family: sans-serif; } + .privacy-note { padding: .75rem; border-left: 4px solid #72aee6; background: #f0f6fc; font-family: sans-serif; }

Mahak Authentication Error

-
-
Exception
-
-
Message
-
getMessage()) ?>
-
Location
-
getFile().':'.$exception->getLine()) ?>
-
-

Stack trace

-
getTraceAsString()) ?>
+ +

Exception

+ + $value): ?> + + +
+ +

WordPress server diagnostics

+ + $value): ?> + + +
+ +

JWT timing claims

+ +

No readable JWT timing claims were available.

+ + + $value): ?> + + +
+ + +

Sanitized stack trace

+
+

JWT contents, request parameters, and function arguments are intentionally omitted.

+ +

Copyable report

+

+
+ diff --git a/test/integration-authentication.php b/test/integration-authentication.php index 2a6c294..f5706c1 100644 --- a/test/integration-authentication.php +++ b/test/integration-authentication.php @@ -258,11 +258,20 @@ function test_debug_error_details(): void { update_option('mahak_debug_mode', 1); - $response = http_request(TEST_SITE_URL.'/mahak/login/?token=not-a-jwt'); + $futureIat = time() + 300; + $token = make_jwt([ + 'iat' => $futureIat, + 'exp' => $futureIat + 600, + 'data' => [ + 'name' => 'Future User', + 'email' => 'future-user@example.test', + ], + ], TEST_SECRET); + $response = http_request(TEST_SITE_URL.'/mahak/login/?token='.rawurlencode($token)); assert_true($response['status'] === 500, 'Debug error response should return HTTP 500.'); assert_true( - strpos($response['body'], 'JWT must contain header, payload, and signature.') !== false, + strpos($response['body'], 'JWT cannot be used before iat.') !== false, 'Debug response did not contain the underlying exception message.' ); assert_true( @@ -270,12 +279,33 @@ function test_debug_error_details(): void 'Debug response did not contain the exception class.' ); assert_true( - strpos($response['body'], 'Stack trace') !== false, - 'Debug response did not contain a stack trace.' + strpos($response['body'], 'WordPress server diagnostics') !== false + && strpos($response['body'], 'Unix timestamp') !== false + && strpos($response['body'], 'WordPress timezone') !== false, + 'Debug response did not contain WordPress server clock and timezone details.' + ); + assert_true( + strpos($response['body'], 'JWT timing claims') !== false + && strpos($response['body'], (string) $futureIat) !== false + && strpos($response['body'], 'seconds vs server') !== false, + 'Debug response did not contain safe JWT timing diagnostics.' + ); + assert_true( + strpos($response['body'], 'Copy Markdown') !== false + && strpos($response['body'], '# Mahak Authentication Error') !== false, + 'Debug response did not contain a copyable Markdown report.' + ); + assert_true( + strpos($response['body'], TEST_SECRET) === false && strpos($response['body'], $token) === false, + 'Debug response exposed the JWT token or signing secret.' + ); + assert_true( + strpos($response['body'], 'Sanitized stack trace') !== false, + 'Debug response did not contain the sanitized stack trace.' ); update_option('mahak_debug_mode', 0); - pass('Debug mode displays full exception details and stack trace'); + pass('Debug report includes clock diagnostics and Markdown without token or secret leakage'); } function test_plugin_update_discovery(): void