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
@@ -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;
}
+136 -2
View File
@@ -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
{
+72 -13
View File
@@ -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; }
</style>
</head>
<body>
<main>
<h1>Mahak Authentication Error</h1>
<dl>
<dt>Exception</dt>
<dd><?= esc_html(get_class($exception)) ?></dd>
<dt>Message</dt>
<dd><?= esc_html($exception->getMessage()) ?></dd>
<dt>Location</dt>
<dd><?= esc_html($exception->getFile().':'.$exception->getLine()) ?></dd>
</dl>
<h2>Stack trace</h2>
<pre><?= esc_html($exception->getTraceAsString()) ?></pre>
<h2>Exception</h2>
<table>
<?php foreach ($report['exception'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<h2>WordPress server diagnostics</h2>
<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>
<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>
</html>