Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f422ac0e4 | ||
|
|
e7d718cd0e | ||
|
|
79564cfaa2 | ||
|
|
322f484144 |
@@ -0,0 +1,4 @@
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.gitea export-ignore
|
||||
/test export-ignore
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Build plugin release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
code: read
|
||||
releases: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate version and build ZIP
|
||||
env:
|
||||
RELEASE_TAG: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
grep -q "Version: ${VERSION}$" dabestaniha-authenticate-bridge.php
|
||||
mkdir -p dist
|
||||
git archive \
|
||||
--format=zip \
|
||||
--prefix=mahak-authenticate-bridge/ \
|
||||
--output=dist/mahak-authenticate-bridge.zip \
|
||||
HEAD
|
||||
|
||||
- name: Publish Gitea release
|
||||
env:
|
||||
GITEA_API_URL: ${{ gitea.api_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
RELEASE_TAG: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
RELEASE_JSON="$(curl --fail-with-body --silent --show-error \
|
||||
--request POST \
|
||||
--header "Authorization: token ${GITEA_TOKEN}" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{\"tag_name\":\"${RELEASE_TAG}\",\"name\":\"${RELEASE_TAG}\",\"draft\":false,\"prerelease\":false}" \
|
||||
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases")"
|
||||
RELEASE_ID="$(printf '%s' "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -n 1 | cut -d: -f2)"
|
||||
test -n "$RELEASE_ID"
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--request POST \
|
||||
--header "Authorization: token ${GITEA_TOKEN}" \
|
||||
--form 'attachment=@dist/mahak-authenticate-bridge.zip' \
|
||||
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=mahak-authenticate-bridge.zip"
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Build plugin release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate version and build ZIP
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${RELEASE_TAG#v}"
|
||||
grep -q "Version: ${VERSION}$" dabestaniha-authenticate-bridge.php
|
||||
mkdir -p build/mahak-authenticate-bridge dist
|
||||
rsync -a \
|
||||
--exclude='.git/' \
|
||||
--exclude='.github/' \
|
||||
--exclude='build/' \
|
||||
--exclude='dist/' \
|
||||
--exclude='test/' \
|
||||
./ build/mahak-authenticate-bridge/
|
||||
cd build
|
||||
zip -qr ../dist/mahak-authenticate-bridge.zip mahak-authenticate-bridge
|
||||
|
||||
- name: Publish GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: gh release create "$RELEASE_TAG" dist/mahak-authenticate-bridge.zip --generate-notes --title "$RELEASE_TAG"
|
||||
@@ -9,14 +9,17 @@ Authenticate WordPress user via json web token from Laravel Application.
|
||||
- Admin settings page
|
||||
- Custom invalid token page
|
||||
- Optional debug mode with full exception details and stack traces
|
||||
- Configurable role for users created through Mahak authentication
|
||||
|
||||
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
|
||||
|
||||
Administrators can check for and install published releases from **Settings > Mahak Authentication**. Updates also appear in WordPress's standard **Plugins** and **Updates** screens.
|
||||
|
||||
To publish an update, change the plugin `Version` header and `MAHAK_AUTHENTICATE_BRIDGE_VERSION` constant to the same version, commit the change, and push a matching tag such as `v2.3.0` to GitHub. The release workflow builds and attaches the ZIP required by the WordPress updater.
|
||||
The **New user role** setting defaults to WordPress Default, using the site's current global new-user role without changing it. Administrators can select a different role only for accounts created by this plugin; existing users keep their current roles.
|
||||
|
||||
To publish an update, change the plugin `Version` header and `MAHAK_AUTHENTICATE_BRIDGE_VERSION` constant to the same version, commit the change, and push a matching tag such as `v2.4.0` to Mahgit. The Gitea Actions workflow builds and attaches the ZIP required by the WordPress updater.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
/**
|
||||
* Plugin Name: Mahak Authenticate Bridge
|
||||
* Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge
|
||||
* Plugin URI: https://mahgit.ir/dabestaniha/mahak-authenticate-bridge
|
||||
* Description: Authenticate WordPress user via json web token from Mahakiha Application.
|
||||
* Version: 2.2.0
|
||||
* Version: 2.3.1
|
||||
* Requires PHP: 7.4
|
||||
* Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge
|
||||
* Update URI: https://mahgit.ir/dabestaniha/mahak-authenticate-bridge
|
||||
* Author: Dabestaniha
|
||||
*
|
||||
* Example: https://wordpress.test/mahak/login/?token=eyJ0...
|
||||
@@ -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.3.1');
|
||||
define('MAHAK_AUTHENTICATE_BRIDGE_FILE', __FILE__);
|
||||
|
||||
require_once __DIR__.'/src/Autoloader.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');
|
||||
@@ -58,7 +64,20 @@ class AuthenticateController extends InvocableController
|
||||
$username .= '_'.wp_generate_password(4, false);
|
||||
}
|
||||
|
||||
$user_id = wp_create_user($username, $random_password, $email);
|
||||
$configured_role = sanitize_key((string) get_configured_option('new-user-role', 'wordpress_default'));
|
||||
|
||||
if ($configured_role === 'wordpress_default') {
|
||||
$configured_role = sanitize_key((string) get_option('default_role', 'subscriber'));
|
||||
}
|
||||
|
||||
$role = get_role($configured_role) === null ? 'subscriber' : $configured_role;
|
||||
$user_id = wp_insert_user([
|
||||
'user_login' => $username,
|
||||
'user_pass' => $random_password,
|
||||
'user_email' => $email,
|
||||
'display_name' => $name,
|
||||
'role' => $role,
|
||||
]);
|
||||
|
||||
if (is_wp_error($user_id)) {
|
||||
throw new RuntimeException(
|
||||
@@ -66,16 +85,8 @@ class AuthenticateController extends InvocableController
|
||||
);
|
||||
}
|
||||
|
||||
$updated_user_id = wp_update_user([
|
||||
'ID' => $user_id,
|
||||
'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()
|
||||
);
|
||||
}
|
||||
// Enforce the configured role after user_register callbacks from other plugins have completed.
|
||||
(new \WP_User($user_id))->set_role($role);
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
@@ -85,7 +96,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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ class SettingsPageFormController extends InvocableController
|
||||
return (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
};
|
||||
$args['default'] = 0;
|
||||
} elseif ($configured === 'new-user-role') {
|
||||
$args['sanitize_callback'] = fn ($value): string => $this->sanitize_new_user_role($value);
|
||||
$args['default'] = 'wordpress_default';
|
||||
}
|
||||
|
||||
register_setting(config('mahak.settings-group'), $options_key, $args);
|
||||
@@ -58,6 +61,14 @@ class SettingsPageFormController extends InvocableController
|
||||
config('mahak.section-id')
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
config('mahak.options.new-user-role'),
|
||||
config('mahak.translations.new-user-role'),
|
||||
fn () => $this->new_user_role(),
|
||||
config('mahak.settings-page'),
|
||||
config('mahak.section-id')
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
config('mahak.options.debug-mode'),
|
||||
config('mahak.translations.debug-mode'),
|
||||
@@ -130,4 +141,54 @@ class SettingsPageFormController extends InvocableController
|
||||
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>';
|
||||
}
|
||||
|
||||
public function new_user_role(): void
|
||||
{
|
||||
$name = config('mahak.options.new-user-role');
|
||||
$value = sanitize_key((string) get_option($name, 'wordpress_default'));
|
||||
$roles = wp_roles()->get_names();
|
||||
|
||||
if ($value !== 'wordpress_default' && !isset($roles[$value])) {
|
||||
$value = 'wordpress_default';
|
||||
}
|
||||
|
||||
echo "<select name='".esc_attr($name)."'>";
|
||||
$defaultRole = sanitize_key((string) get_option('default_role', 'subscriber'));
|
||||
$defaultRoleLabel = $roles[$defaultRole] ?? $defaultRole;
|
||||
$translatedDefaultRole = function_exists('translate_user_role')
|
||||
? translate_user_role($defaultRoleLabel)
|
||||
: $defaultRoleLabel;
|
||||
echo "<option value='wordpress_default' ".selected($value, 'wordpress_default', false).'>';
|
||||
echo esc_html(config('mahak.translations.wordpress-default-role').' — '.$translatedDefaultRole).'</option>';
|
||||
|
||||
foreach ($roles as $role => $label) {
|
||||
$translatedLabel = function_exists('translate_user_role') ? translate_user_role($label) : $label;
|
||||
echo "<option value='".esc_attr($role)."' ".selected($value, $role, false).'>';
|
||||
echo esc_html($translatedLabel).'</option>';
|
||||
}
|
||||
|
||||
echo '</select>';
|
||||
echo '<p class="description">'.esc_html(config('mahak.translations.new-user-role-description')).'</p>';
|
||||
}
|
||||
|
||||
public function sanitize_new_user_role($value): string
|
||||
{
|
||||
$role = sanitize_key((string) $value);
|
||||
|
||||
if ($role === 'wordpress_default') {
|
||||
return $role;
|
||||
}
|
||||
|
||||
if (get_role($role) !== null) {
|
||||
return $role;
|
||||
}
|
||||
|
||||
add_settings_error(
|
||||
config('mahak.options.new-user-role'),
|
||||
'mahak_invalid_new_user_role',
|
||||
'نقش انتخابشده معتبر نیست. نقش پیشفرض وردپرس استفاده شد.'
|
||||
);
|
||||
|
||||
return 'wordpress_default';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace Dabestaniha\AuthenticateBridge\App\Support;
|
||||
|
||||
final class PluginUpdater
|
||||
{
|
||||
private const API_URL = 'https://api.github.com/repos/dabestaniha/mahak-authenticate-bridge/releases/latest';
|
||||
private const API_URL = 'https://mahgit.ir/api/v1/repos/dabestaniha/mahak-authenticate-bridge/releases/latest';
|
||||
private const RELEASE_ASSET = 'mahak-authenticate-bridge.zip';
|
||||
private const RELEASE_CACHE = 'mahak_authenticate_bridge_release';
|
||||
private const RELEASE_CACHE = 'mahak_authenticate_bridge_gitea_release';
|
||||
private const CHECK_ACTION = 'mahak_authenticate_check_updates';
|
||||
|
||||
private static ?self $instance = null;
|
||||
@@ -74,7 +74,7 @@ final class PluginUpdater
|
||||
'slug' => $this->slug,
|
||||
'version' => $this->releaseVersion($release),
|
||||
'author' => 'Dabestaniha',
|
||||
'homepage' => $release['html_url'] ?? 'https://github.com/dabestaniha/mahak-authenticate-bridge',
|
||||
'homepage' => $release['html_url'] ?? 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge',
|
||||
'download_link' => $package,
|
||||
'requires_php' => '7.4',
|
||||
'sections' => [
|
||||
@@ -177,30 +177,30 @@ final class PluginUpdater
|
||||
|
||||
$response = wp_remote_get(self::API_URL, [
|
||||
'headers' => [
|
||||
'Accept' => 'application/vnd.github+json',
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'Mahak-Authenticate-Bridge/'.MAHAK_AUTHENTICATE_BRIDGE_VERSION,
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new \WP_Error('mahak_update_request_failed', 'Could not contact GitHub to check for plugin updates.');
|
||||
return new \WP_Error('mahak_update_request_failed', 'Could not contact Mahgit to check for plugin updates.');
|
||||
}
|
||||
|
||||
if (wp_remote_retrieve_response_code($response) !== 200) {
|
||||
return new \WP_Error('mahak_update_response_invalid', 'No published plugin release is currently available on GitHub.');
|
||||
return new \WP_Error('mahak_update_response_invalid', 'No published plugin release is currently available on Mahgit.');
|
||||
}
|
||||
|
||||
$release = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!is_array($release) || !empty($release['draft']) || !empty($release['prerelease'])) {
|
||||
return new \WP_Error('mahak_update_release_invalid', 'GitHub returned invalid plugin release information.');
|
||||
return new \WP_Error('mahak_update_release_invalid', 'Mahgit returned invalid plugin release information.');
|
||||
}
|
||||
|
||||
if ($this->releaseVersion($release) === '' || $this->releasePackage($release) === null) {
|
||||
return new \WP_Error(
|
||||
'mahak_update_asset_missing',
|
||||
'The latest GitHub release does not contain a valid mahak-authenticate-bridge.zip package.'
|
||||
'The latest Mahgit release does not contain a valid mahak-authenticate-bridge.zip package.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ final class PluginUpdater
|
||||
if (
|
||||
$url !== ''
|
||||
&& wp_parse_url($url, PHP_URL_SCHEME) === 'https'
|
||||
&& wp_parse_url($url, PHP_URL_HOST) === 'github.com'
|
||||
&& wp_parse_url($url, PHP_URL_HOST) === 'mahgit.ir'
|
||||
&& strpos($path, '/dabestaniha/mahak-authenticate-bridge/releases/download/') === 0
|
||||
) {
|
||||
return $url;
|
||||
|
||||
@@ -11,6 +11,7 @@ return [
|
||||
'user-identifier' => 'mahak_user_identifier',
|
||||
'login-route' => 'mahak_login_route',
|
||||
'after-login-route' => 'mahak_after_login_route',
|
||||
'new-user-role' => 'mahak_new_user_role',
|
||||
'debug-mode' => 'mahak_debug_mode',
|
||||
],
|
||||
|
||||
@@ -38,6 +39,9 @@ return [
|
||||
'user-identifier' => 'فیلد شناسایی کاربر',
|
||||
'login-route' => 'مسیر لاگین ماهک',
|
||||
'after-login-route' => 'مسیر بعد از لاگین',
|
||||
'new-user-role' => 'نقش کاربر جدید',
|
||||
'wordpress-default-role' => 'پیشفرض وردپرس',
|
||||
'new-user-role-description' => 'در حالت پیشفرض وردپرس، نقش عمومی کاربران جدید استفاده میشود. نقش انتخابی دیگر فقط به کاربران ساختهشده توسط این افزونه اختصاص مییابد.',
|
||||
'debug-mode' => 'حالت اشکالزدایی',
|
||||
'debug-mode-description' => 'در صورت بروز خطا، جزئیات کامل فنی در صفحه نمایش داده شود. این گزینه را فقط هنگام عیبیابی فعال کنید.',
|
||||
|
||||
|
||||
+136
-2
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ const TEST_PLUGIN = 'mahak-authenticate-bridge/dabestaniha-authenticate-bridge.p
|
||||
const TEST_SECRET = 'integration-test-secret';
|
||||
const TEST_EMAIL = 'mahak-auth-user@example.test';
|
||||
const TEST_NAME = 'Mahak Auth User';
|
||||
const TEST_ROLE_EMAIL = 'mahak-role-user@example.test';
|
||||
|
||||
function fail(string $message): void
|
||||
{
|
||||
@@ -86,6 +87,8 @@ function activate_and_configure_plugin(): void
|
||||
update_option('mahak_user_identifier', 'email');
|
||||
update_option('mahak_login_route', 'mahak/login');
|
||||
update_option('mahak_after_login_route', 'wp-admin/profile.php');
|
||||
update_option('default_role', 'subscriber');
|
||||
update_option('mahak_new_user_role', 'wordpress_default');
|
||||
|
||||
pass('Plugin activated and configured');
|
||||
}
|
||||
@@ -213,6 +216,10 @@ function test_valid_authentication(): void
|
||||
|
||||
assert_true($user !== false, 'Authenticated user was not created.');
|
||||
assert_true($user->display_name === TEST_NAME, 'Authenticated user display name was not saved.');
|
||||
assert_true(
|
||||
$user->roles === ['subscriber'],
|
||||
'New user did not receive the plugin-configured Subscriber role.'
|
||||
);
|
||||
|
||||
$cookieHeader = cookie_header_from_response($response['headers']);
|
||||
$profileResponse = http_request(TEST_SITE_URL.'/wp-admin/profile.php', $cookieHeader);
|
||||
@@ -222,7 +229,38 @@ function test_valid_authentication(): void
|
||||
'Issued cookies did not authenticate a follow-up WordPress admin request.'
|
||||
);
|
||||
|
||||
pass('Valid JWT creates/logs in user and issued cookies authenticate follow-up request');
|
||||
pass('Valid JWT creates a Subscriber and issued cookies authenticate follow-up requests');
|
||||
}
|
||||
|
||||
function test_configured_new_user_role_is_enforced(): void
|
||||
{
|
||||
$smsRoleOverride = function (int $userId): void {
|
||||
(new WP_User($userId))->set_role('administrator');
|
||||
};
|
||||
add_action('user_register', $smsRoleOverride);
|
||||
|
||||
$controller = new Dabestaniha\AuthenticateBridge\App\Http\Controllers\AuthenticateController();
|
||||
$userId = $controller->find_or_create_user('email', TEST_ROLE_EMAIL, 'Role Test User');
|
||||
|
||||
remove_action('user_register', $smsRoleOverride);
|
||||
|
||||
$user = get_userdata($userId);
|
||||
assert_true(
|
||||
$user !== false && $user->roles === ['subscriber'],
|
||||
'Configured role was not enforced after another plugin changed the role during user_register.'
|
||||
);
|
||||
|
||||
$user->set_role('author');
|
||||
$existingUserId = $controller->find_or_create_user('email', TEST_ROLE_EMAIL, 'Role Test User');
|
||||
$existingUser = get_userdata($existingUserId);
|
||||
|
||||
assert_true($existingUserId === $userId, 'Existing user was not reused.');
|
||||
assert_true(
|
||||
$existingUser !== false && $existingUser->roles === ['author'],
|
||||
'Existing user role was changed during authentication.'
|
||||
);
|
||||
|
||||
pass('Configured role overrides creation hooks without changing existing users');
|
||||
}
|
||||
|
||||
function test_invalid_authentication(): void
|
||||
@@ -258,11 +296,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,27 +317,48 @@ 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
|
||||
{
|
||||
$packageUrl = 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/download/v2.3.0/mahak-authenticate-bridge.zip';
|
||||
$packageUrl = 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge/releases/download/v2.4.0/mahak-authenticate-bridge.zip';
|
||||
$mockRelease = function ($response, array $request, string $url) use ($packageUrl) {
|
||||
if ($url !== 'https://api.github.com/repos/dabestaniha/mahak-authenticate-bridge/releases/latest') {
|
||||
if ($url !== 'https://mahgit.ir/api/v1/repos/dabestaniha/mahak-authenticate-bridge/releases/latest') {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return [
|
||||
'headers' => [],
|
||||
'body' => json_encode([
|
||||
'tag_name' => 'v2.3.0',
|
||||
'html_url' => 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/tag/v2.3.0',
|
||||
'tag_name' => 'v2.4.0',
|
||||
'html_url' => 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge/releases/tag/v2.4.0',
|
||||
'body' => 'Test release',
|
||||
'draft' => false,
|
||||
'prerelease' => false,
|
||||
@@ -305,7 +373,7 @@ function test_plugin_update_discovery(): void
|
||||
];
|
||||
};
|
||||
|
||||
delete_site_transient('mahak_authenticate_bridge_release');
|
||||
delete_site_transient('mahak_authenticate_bridge_gitea_release');
|
||||
add_filter('pre_http_request', $mockRelease, 10, 3);
|
||||
|
||||
$updates = apply_filters('pre_set_site_transient_update_plugins', (object) [
|
||||
@@ -315,11 +383,11 @@ function test_plugin_update_discovery(): void
|
||||
|
||||
remove_filter('pre_http_request', $mockRelease, 10);
|
||||
|
||||
assert_true(isset($updates->response[TEST_PLUGIN]), 'A newer GitHub release was not offered as a WordPress update.');
|
||||
assert_true($updates->response[TEST_PLUGIN]->new_version === '2.3.0', 'The offered plugin version was incorrect.');
|
||||
assert_true(isset($updates->response[TEST_PLUGIN]), 'A newer Mahgit release was not offered as a WordPress update.');
|
||||
assert_true($updates->response[TEST_PLUGIN]->new_version === '2.4.0', 'The offered plugin version was incorrect.');
|
||||
assert_true($updates->response[TEST_PLUGIN]->package === $packageUrl, 'The release package URL was incorrect.');
|
||||
|
||||
pass('Published GitHub releases are discovered by the WordPress updater');
|
||||
pass('Published Mahgit releases are discovered by the WordPress updater');
|
||||
}
|
||||
|
||||
boot_wordpress();
|
||||
@@ -327,6 +395,7 @@ install_wordpress();
|
||||
activate_and_configure_plugin();
|
||||
wait_for_http();
|
||||
test_valid_authentication();
|
||||
test_configured_new_user_role_is_enforced();
|
||||
test_invalid_authentication();
|
||||
test_debug_error_details();
|
||||
test_plugin_update_discovery();
|
||||
|
||||
Reference in New Issue
Block a user