fix: add safe clock diagnostics to debug reports

This commit is contained in:
2026-08-24 09:53:16 +03:30
parent 656fc907a9
commit 322f484144
6 changed files with 282 additions and 24 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Authenticate WordPress user via json web token from Laravel Application.
- Custom invalid token page - Custom invalid token page
- Optional debug mode with full exception details and stack traces - 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 ## Updates
+2 -2
View File
@@ -4,7 +4,7 @@
* Plugin Name: Mahak Authenticate Bridge * Plugin Name: Mahak Authenticate Bridge
* Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge
* Description: Authenticate WordPress user via json web token from Mahakiha Application. * Description: Authenticate WordPress user via json web token from Mahakiha Application.
* Version: 2.2.0 * Version: 2.2.1
* Requires PHP: 7.4 * Requires PHP: 7.4
* Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge * Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge
* Author: Dabestaniha * Author: Dabestaniha
@@ -18,7 +18,7 @@ use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageMenuControll
use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageFormController; use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageFormController;
use Dabestaniha\AuthenticateBridge\App\Support\PluginUpdater; 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__); define('MAHAK_AUTHENTICATE_BRIDGE_FILE', __FILE__);
require_once __DIR__.'/src/Autoloader.php'; require_once __DIR__.'/src/Autoloader.php';
@@ -10,10 +10,16 @@ use UnexpectedValueException;
class AuthenticateController extends InvocableController class AuthenticateController extends InvocableController
{ {
private array $debugContext = [];
public function __invoke(MahakAuthenticateRequest $request) public function __invoke(MahakAuthenticateRequest $request)
{ {
$token = $request->string('token'); $token = $request->string('token');
if (mahak_debug_enabled()) {
$this->debugContext['jwt_timestamps'] = $this->inspectJwtTimestamps($token);
}
$payload = $this->decrypt_jwt_token($token); $payload = $this->decrypt_jwt_token($token);
$identifier_name = get_configured_option('user-identifier'); $identifier_name = get_configured_option('user-identifier');
@@ -85,7 +91,36 @@ class AuthenticateController extends InvocableController
$status = $exception->getCode() === 403 ? 403 : 500; $status = $exception->getCode() === 403 ? 403 : 500;
$fallbackView = $exception instanceof UnexpectedValueException ? 'invalid-token' : 'server-error'; $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;
} }
+136 -2
View File
@@ -20,11 +20,16 @@ if (!function_exists('mahak_debug_enabled')) {
} }
if (!function_exists('mahak_render_error')) { 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()) { if (mahak_debug_enabled()) {
wp_die( wp_die(
view('debug-error', ['exception' => $exception]), view('debug-error', ['report' => mahak_build_debug_report($exception, $context)]),
'Mahak Authentication Error', 'Mahak Authentication Error',
['response' => $status] ['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')) { if (!function_exists('dd')) {
function dd(): void function dd(): void
{ {
+72 -13
View File
@@ -8,24 +8,83 @@
body { margin: 2rem; color: #1d2327; background: #f0f0f1; font: 14px/1.5 monospace; } 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); } 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; } h1 { margin-top: 0; color: #d63638; font: 24px/1.3 sans-serif; }
dt { margin-top: 1rem; font-weight: 700; } h2 { margin-top: 2rem; font: 20px/1.3 sans-serif; }
dd { margin: .25rem 0 0; overflow-wrap: anywhere; } table { width: 100%; border-collapse: collapse; }
pre { overflow: auto; padding: 1rem; color: #f0f0f1; background: #1d2327; white-space: pre-wrap; } 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; }
</style> </style>
</head> </head>
<body> <body>
<main> <main>
<h1>Mahak Authentication Error</h1> <h1>Mahak Authentication Error</h1>
<dl>
<dt>Exception</dt> <h2>Exception</h2>
<dd><?= esc_html(get_class($exception)) ?></dd> <table>
<dt>Message</dt> <?php foreach ($report['exception'] as $label => $value): ?>
<dd><?= esc_html($exception->getMessage()) ?></dd> <tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<dt>Location</dt> <?php endforeach; ?>
<dd><?= esc_html($exception->getFile().':'.$exception->getLine()) ?></dd> </table>
</dl>
<h2>Stack trace</h2> <h2>WordPress server diagnostics</h2>
<pre><?= esc_html($exception->getTraceAsString()) ?></pre> <table>
<?php foreach ($report['server'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<h2>JWT timing claims</h2>
<?php if ($report['jwt_timestamps'] === []): ?>
<p>No readable JWT timing claims were available.</p>
<?php else: ?>
<table>
<?php foreach ($report['jwt_timestamps'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>Sanitized stack trace</h2>
<pre><?= esc_html($report['trace']) ?></pre>
<p class="privacy-note">JWT contents, request parameters, and function arguments are intentionally omitted.</p>
<h2>Copyable report</h2>
<p><button type="button" id="copy-markdown">Copy Markdown</button><span id="copy-status" role="status"></span></p>
<textarea id="debug-markdown" readonly><?= esc_textarea($report['markdown']) ?></textarea>
</main> </main>
<script>
(function () {
var button = document.getElementById('copy-markdown');
var report = document.getElementById('debug-markdown');
var status = document.getElementById('copy-status');
button.addEventListener('click', function () {
var copied = function () { status.textContent = 'Copied.'; };
var failed = function () {
report.focus();
report.select();
status.textContent = 'Select the report and copy it manually.';
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(report.value).then(copied, failed);
return;
}
report.focus();
report.select();
try {
document.execCommand('copy') ? copied() : failed();
} catch (error) {
failed();
}
});
}());
</script>
</body> </body>
</html> </html>
+35 -5
View File
@@ -258,11 +258,20 @@ function test_debug_error_details(): void
{ {
update_option('mahak_debug_mode', 1); 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($response['status'] === 500, 'Debug error response should return HTTP 500.');
assert_true( 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.' 'Debug response did not contain the underlying exception message.'
); );
assert_true( assert_true(
@@ -270,12 +279,33 @@ function test_debug_error_details(): void
'Debug response did not contain the exception class.' 'Debug response did not contain the exception class.'
); );
assert_true( assert_true(
strpos($response['body'], 'Stack trace') !== false, strpos($response['body'], 'WordPress server diagnostics') !== false
'Debug response did not contain a stack trace.' && 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); 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 function test_plugin_update_discovery(): void