feat: add admin-controlled authentication debug mode
This commit is contained in:
@@ -8,6 +8,9 @@ Authenticate WordPress user via json web token from Laravel Application.
|
|||||||
- Login via JWT token
|
- Login via JWT token
|
||||||
- Admin settings page
|
- Admin settings page
|
||||||
- Custom invalid token page
|
- 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.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -32,4 +35,4 @@ Payload JWT:
|
|||||||
"username" : "test-user"
|
"username" : "test-user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/**
|
/**
|
||||||
* Plugin Name: Mahak Authenticate Bridge
|
* Plugin Name: 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.0
|
* Version: 2.1.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: Dabestaniha
|
* Author: Dabestaniha
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace Dabestaniha\AuthenticateBridge\App\Http\Controllers;
|
|||||||
|
|
||||||
use Dabestaniha\AuthenticateBridge\App\Http\Requests\MahakAuthenticateRequest;
|
use Dabestaniha\AuthenticateBridge\App\Http\Requests\MahakAuthenticateRequest;
|
||||||
use Dabestaniha\AuthenticateBridge\App\Support\JwtDecoder;
|
use Dabestaniha\AuthenticateBridge\App\Support\JwtDecoder;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
use UnexpectedValueException;
|
use UnexpectedValueException;
|
||||||
|
|
||||||
class AuthenticateController extends InvocableController
|
class AuthenticateController extends InvocableController
|
||||||
@@ -31,18 +33,14 @@ class AuthenticateController extends InvocableController
|
|||||||
{
|
{
|
||||||
$jwt_secret = get_configured_option('jwt-secret');
|
$jwt_secret = get_configured_option('jwt-secret');
|
||||||
|
|
||||||
try {
|
$decoded = JwtDecoder::decodeHs256($token, $jwt_secret);
|
||||||
$decoded = JwtDecoder::decodeHs256($token, $jwt_secret);
|
$data = $decoded['data'] ?? null;
|
||||||
$data = $decoded['data'] ?? null;
|
|
||||||
|
|
||||||
if (!is_array($data)) {
|
if (!is_array($data)) {
|
||||||
throw new UnexpectedValueException('JWT data must be an object.');
|
throw new UnexpectedValueException('JWT data must be an object.');
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function find_or_create_user(string $identifier_name, string $identifier_value, string $name): int
|
public function find_or_create_user(string $identifier_name, string $identifier_value, string $name): int
|
||||||
@@ -63,17 +61,33 @@ class AuthenticateController extends InvocableController
|
|||||||
$user_id = wp_create_user($username, $random_password, $email);
|
$user_id = wp_create_user($username, $random_password, $email);
|
||||||
|
|
||||||
if (is_wp_error($user_id)) {
|
if (is_wp_error($user_id)) {
|
||||||
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]);
|
throw new RuntimeException(
|
||||||
|
'WordPress could not create the user: '.$user_id->get_error_message()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_update_user([
|
$updated_user_id = wp_update_user([
|
||||||
'ID' => $user_id,
|
'ID' => $user_id,
|
||||||
'display_name' => $name,
|
'display_name' => $name,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($updated_user_id)) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'WordPress could not update the user: '.$updated_user_id->get_error_message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $user_id;
|
return $user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function handleException(Throwable $exception): void
|
||||||
|
{
|
||||||
|
$status = $exception->getCode() === 403 ? 403 : 500;
|
||||||
|
$fallbackView = $exception instanceof UnexpectedValueException ? 'invalid-token' : 'server-error';
|
||||||
|
|
||||||
|
mahak_render_error($exception, $status, $fallbackView);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function login_user(int $user_id): bool
|
public function login_user(int $user_id): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,30 +4,41 @@ namespace Dabestaniha\AuthenticateBridge\App\Http\Controllers;
|
|||||||
|
|
||||||
use Dabestaniha\AuthenticateBridge\App\Http\Requests\BaseFormRequest;
|
use Dabestaniha\AuthenticateBridge\App\Http\Requests\BaseFormRequest;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
abstract class InvocableController
|
abstract class InvocableController
|
||||||
{
|
{
|
||||||
public static function resolve(): void
|
public static function resolve(): void
|
||||||
{
|
{
|
||||||
$reflection = new ReflectionMethod(static::class, '__invoke');
|
|
||||||
|
|
||||||
$args = [];
|
|
||||||
foreach ($reflection->getParameters() as $param) {
|
|
||||||
$class = $param->getType()->getName();
|
|
||||||
|
|
||||||
if (is_subclass_of($class, BaseFormRequest::class)) {
|
|
||||||
$request = new $class();
|
|
||||||
|
|
||||||
if (!$request->isRequestForThisRoute()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->validate();
|
|
||||||
$args[] = $request;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$controller = new static();
|
$controller = new static();
|
||||||
$controller(...$args);
|
|
||||||
|
try {
|
||||||
|
$reflection = new ReflectionMethod(static::class, '__invoke');
|
||||||
|
|
||||||
|
$args = [];
|
||||||
|
foreach ($reflection->getParameters() as $param) {
|
||||||
|
$class = $param->getType()->getName();
|
||||||
|
|
||||||
|
if (is_subclass_of($class, BaseFormRequest::class)) {
|
||||||
|
$request = new $class();
|
||||||
|
|
||||||
|
if (!$request->isRequestForThisRoute()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate();
|
||||||
|
$args[] = $request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$controller(...$args);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$controller->handleException($exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
protected function handleException(Throwable $exception): void
|
||||||
|
{
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,16 @@ class SettingsPageFormController extends InvocableController
|
|||||||
public function __invoke()
|
public function __invoke()
|
||||||
{
|
{
|
||||||
foreach (config('mahak.options') as $configured => $options_key) {
|
foreach (config('mahak.options') as $configured => $options_key) {
|
||||||
register_setting(config('mahak.settings-group'), $options_key);
|
$args = [];
|
||||||
|
|
||||||
|
if ($configured === 'debug-mode') {
|
||||||
|
$args['sanitize_callback'] = function ($value): int {
|
||||||
|
return (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
};
|
||||||
|
$args['default'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_setting(config('mahak.settings-group'), $options_key, $args);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_settings_section(
|
add_settings_section(
|
||||||
@@ -49,6 +58,14 @@ class SettingsPageFormController extends InvocableController
|
|||||||
config('mahak.section-id')
|
config('mahak.section-id')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
config('mahak.options.debug-mode'),
|
||||||
|
config('mahak.translations.debug-mode'),
|
||||||
|
fn () => $this->debug_mode(),
|
||||||
|
config('mahak.settings-page'),
|
||||||
|
config('mahak.section-id')
|
||||||
|
);
|
||||||
|
|
||||||
$loginRouteOptionName = config('mahak.options.login-route');
|
$loginRouteOptionName = config('mahak.options.login-route');
|
||||||
add_filter("pre_update_option_{$loginRouteOptionName}", function ($value) {
|
add_filter("pre_update_option_{$loginRouteOptionName}", function ($value) {
|
||||||
return trim($value, '/');
|
return trim($value, '/');
|
||||||
@@ -103,4 +120,14 @@ class SettingsPageFormController extends InvocableController
|
|||||||
echo "<input type='text' name='$name' value='$value' class='regular-text' />";
|
echo "<input type='text' name='$name' value='$value' class='regular-text' />";
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function debug_mode(): void
|
||||||
|
{
|
||||||
|
$name = config('mahak.options.debug-mode');
|
||||||
|
$enabled = (bool) get_option($name, false);
|
||||||
|
|
||||||
|
echo "<input type='hidden' name='".esc_attr($name)."' value='0' />";
|
||||||
|
echo "<label><input type='checkbox' name='".esc_attr($name)."' value='1' ".checked($enabled, true, false).' /> ';
|
||||||
|
echo esc_html(config('mahak.translations.debug-mode-description')).'</label>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Dabestaniha\AuthenticateBridge\App\Http\Requests;
|
namespace Dabestaniha\AuthenticateBridge\App\Http\Requests;
|
||||||
|
|
||||||
use Dabestaniha\AuthenticateBridge\App\Support\Collection;
|
use Dabestaniha\AuthenticateBridge\App\Support\Collection;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
abstract class BaseFormRequest
|
abstract class BaseFormRequest
|
||||||
{
|
{
|
||||||
@@ -44,7 +45,10 @@ abstract class BaseFormRequest
|
|||||||
|
|
||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
if (!$this->validateValue($value, $rule)) {
|
if (!$this->validateValue($value, $rule)) {
|
||||||
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 403]);
|
throw new UnexpectedValueException(
|
||||||
|
sprintf('The request field "%s" failed the "%s" validation rule.', $attribute, $rule),
|
||||||
|
403
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$data[$attribute] = $value;
|
$data[$attribute] = $value;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ return [
|
|||||||
'user-identifier' => 'mahak_user_identifier',
|
'user-identifier' => 'mahak_user_identifier',
|
||||||
'login-route' => 'mahak_login_route',
|
'login-route' => 'mahak_login_route',
|
||||||
'after-login-route' => 'mahak_after_login_route',
|
'after-login-route' => 'mahak_after_login_route',
|
||||||
|
'debug-mode' => 'mahak_debug_mode',
|
||||||
],
|
],
|
||||||
|
|
||||||
'user-identifiers' => [
|
'user-identifiers' => [
|
||||||
@@ -37,7 +38,9 @@ return [
|
|||||||
'user-identifier' => 'فیلد شناسایی کاربر',
|
'user-identifier' => 'فیلد شناسایی کاربر',
|
||||||
'login-route' => 'مسیر لاگین ماهک',
|
'login-route' => 'مسیر لاگین ماهک',
|
||||||
'after-login-route' => 'مسیر بعد از لاگین',
|
'after-login-route' => 'مسیر بعد از لاگین',
|
||||||
|
'debug-mode' => 'حالت اشکالزدایی',
|
||||||
|
'debug-mode-description' => 'در صورت بروز خطا، جزئیات کامل فنی در صفحه نمایش داده شود. این گزینه را فقط هنگام عیبیابی فعال کنید.',
|
||||||
|
|
||||||
'new-user' => 'کاربر جدید',
|
'new-user' => 'کاربر جدید',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
+24
-1
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (!function_exists('view')) {
|
if (!function_exists('view')) {
|
||||||
function view(string $view): string
|
function view(string $view, array $data = []): string
|
||||||
{
|
{
|
||||||
$basedir = plugin_dir_path(__FILE__);
|
$basedir = plugin_dir_path(__FILE__);
|
||||||
|
extract($data, EXTR_SKIP);
|
||||||
ob_start();
|
ob_start();
|
||||||
include "{$basedir}resources/views/$view.php";
|
include "{$basedir}resources/views/$view.php";
|
||||||
|
|
||||||
@@ -11,6 +12,28 @@ if (!function_exists('view')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mahak_debug_enabled')) {
|
||||||
|
function mahak_debug_enabled(): bool
|
||||||
|
{
|
||||||
|
return (bool) get_configured_option('debug-mode', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mahak_render_error')) {
|
||||||
|
function mahak_render_error(\Throwable $exception, int $status = 500, string $fallbackView = 'server-error'): void
|
||||||
|
{
|
||||||
|
if (mahak_debug_enabled()) {
|
||||||
|
wp_die(
|
||||||
|
view('debug-error', ['exception' => $exception]),
|
||||||
|
'Mahak Authentication Error',
|
||||||
|
['response' => $status]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_die(view($fallbackView), 'Authentication Error', ['response' => $status]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!function_exists('dd')) {
|
if (!function_exists('dd')) {
|
||||||
function dd(): void
|
function dd(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Mahak Authentication Error</title>
|
||||||
|
<style>
|
||||||
|
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; }
|
||||||
|
</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>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<!-- views/invalid-token.php -->
|
<!-- views/server-error.php -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -11,4 +11,4 @@
|
|||||||
<p>مشکلی پیش آمده، لطفا تا دقایق دیگر دوباره امتحان کنید.</p>
|
<p>مشکلی پیش آمده، لطفا تا دقایق دیگر دوباره امتحان کنید.</p>
|
||||||
<a href="<?= home_url() ?>">بازگشت به صفحه اصلی</a>
|
<a href="<?= home_url() ?>">بازگشت به صفحه اصلی</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -246,8 +246,36 @@ function test_invalid_authentication(): void
|
|||||||
!header_contains($response['headers'], 'Set-Cookie: wordpress_logged_in_'),
|
!header_contains($response['headers'], 'Set-Cookie: wordpress_logged_in_'),
|
||||||
'Invalid authentication request issued a logged-in cookie.'
|
'Invalid authentication request issued a logged-in cookie.'
|
||||||
);
|
);
|
||||||
|
assert_true(
|
||||||
|
strpos($response['body'], 'JWT signature is invalid.') === false,
|
||||||
|
'Debug details were exposed while debug mode was disabled.'
|
||||||
|
);
|
||||||
|
|
||||||
pass('Invalid JWT is rejected without login cookies');
|
pass('Invalid JWT is rejected without login cookies or debug details');
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_debug_error_details(): void
|
||||||
|
{
|
||||||
|
update_option('mahak_debug_mode', 1);
|
||||||
|
|
||||||
|
$response = http_request(TEST_SITE_URL.'/mahak/login/?token=not-a-jwt');
|
||||||
|
|
||||||
|
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,
|
||||||
|
'Debug response did not contain the underlying exception message.'
|
||||||
|
);
|
||||||
|
assert_true(
|
||||||
|
strpos($response['body'], 'UnexpectedValueException') !== false,
|
||||||
|
'Debug response did not contain the exception class.'
|
||||||
|
);
|
||||||
|
assert_true(
|
||||||
|
strpos($response['body'], 'Stack trace') !== false,
|
||||||
|
'Debug response did not contain a stack trace.'
|
||||||
|
);
|
||||||
|
|
||||||
|
update_option('mahak_debug_mode', 0);
|
||||||
|
pass('Debug mode displays full exception details and stack trace');
|
||||||
}
|
}
|
||||||
|
|
||||||
boot_wordpress();
|
boot_wordpress();
|
||||||
@@ -256,5 +284,6 @@ activate_and_configure_plugin();
|
|||||||
wait_for_http();
|
wait_for_http();
|
||||||
test_valid_authentication();
|
test_valid_authentication();
|
||||||
test_invalid_authentication();
|
test_invalid_authentication();
|
||||||
|
test_debug_error_details();
|
||||||
|
|
||||||
echo "[OK] Authentication integration test passed\n";
|
echo "[OK] Authentication integration test passed\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user