diff --git a/didit-verify.php b/didit-verify.php index 5b86a91..e95db79 100644 --- a/didit-verify.php +++ b/didit-verify.php @@ -3,7 +3,7 @@ * Plugin Name: Didit Verify * Plugin URI: https://github.com/didit-protocol/plugin-wordpress * Description: Identity verification for WordPress & WooCommerce using the Didit SDK. - * Version: 0.3.3 + * Version: 0.3.4 * Author: Didit * Author URI: https://didit.me * License: GPL-2.0-or-later @@ -18,7 +18,7 @@ exit; } -define('DIDIT_VERIFY_VERSION', '0.3.3'); +define('DIDIT_VERIFY_VERSION', '0.3.4'); define('DIDIT_VERIFY_URL', plugin_dir_url(__FILE__)); define('DIDIT_API_URL', 'https://verification.didit.me/v3/session/'); @@ -1165,7 +1165,11 @@ private function apply_confirmed_status($session_id, array $record, string $stat $record['event_timestamp'] = $timestamp; $this->store_session($session_id, $record); $user_id = (int) $record['user_id']; - if ($user_id && (string) get_user_meta($user_id, '_didit_pending_session_id', true) === $session_id) { + // A new attempt must not prevent revocation of the session still granting access. + if ($user_id && ( + (string) get_user_meta($user_id, '_didit_pending_session_id', true) === $session_id + || (string) get_user_meta($user_id, '_didit_confirmed_session_id', true) === $session_id + )) { update_user_meta($user_id, '_didit_session_id', $session_id); update_user_meta($user_id, '_didit_confirmed_session_id', $session_id); update_user_meta($user_id, '_didit_status', $status); @@ -1194,7 +1198,12 @@ private function confirmed_user_status($user_id): string if (!$session_id || $session_id !== (string) get_user_meta($user_id, '_didit_session_id', true)) { return ''; } - return (string) get_user_meta($user_id, '_didit_status', true); + // Read the recorded decision so a revocation received before an upgrade also applies. + $record = $this->stored_session($session_id); + if (!is_array($record) || (int) ($record['user_id'] ?? 0) !== (int) $user_id) { + return ''; + } + return $this->normalize_status($record['status'] ?? ''); } public function is_user_verified($user_id): bool diff --git a/readme.txt b/readme.txt index f6c55cb..3504d03 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: identity verification, kyc, woocommerce, age verification, id check Requires at least: 6.0 Tested up to: 7.1 Requires PHP: 7.4 -Stable tag: 0.3.3 +Stable tag: 0.3.4 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -210,6 +210,10 @@ Yes. Go to **Settings → Didit Verify → Display Options → Display Mode**. C == Changelog == += 0.3.4 = +* Security: revoke protected-content access when the approved verification is declined, expires, or returns to review, even if another verification has started. +* Keep a newer approved verification unaffected by updates to an older session. + = 0.3.3 = * Add Didit branding and grouped cards to the WordPress settings page. * Show readable verification status badges in Users and WooCommerce order details. @@ -290,6 +294,9 @@ Yes. Go to **Settings → Didit Verify → Display Options → Display Mode**. C == Upgrade Notice == += 0.3.4 = +Security update for protected content: revocations now apply to the verification granting access even when another attempt is pending. + = 0.3.3 = Branded administration and clearer verification results. Includes the server-confirmed approval protections introduced in 0.3.2. diff --git a/tests/test-verification-authorization.php b/tests/test-verification-authorization.php index 527b12b..042cf27 100644 --- a/tests/test-verification-authorization.php +++ b/tests/test-verification-authorization.php @@ -61,4 +61,49 @@ function claim($id, $status = 'Approved') { internal('store_session', $id, $record); internal('apply_confirmed_status', $id, $record, 'Approved', time() - 60); check('Not Started' === internal('stored_session', $id)['status'], 'older signed delivery cannot overwrite newer state'); + +// A second attempt must not prevent revocation of the session granting access. +$pending_id = 'bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee'; +$secret = 'test-only-webhook-secret'; +$GLOBALS['didit_test_options']['didit_webhook_secret'] = $secret; +function signed_status($id, $status, $timestamp) { + $body = json_encode(['session_id' => $id, 'status' => $status, + 'timestamp' => $timestamp, 'webhook_type' => 'status.updated']); + return Didit_Verify::init()->rest_webhook(new Didit_Test_Request($body, [ + 'X-Signature' => hash_hmac('sha256', $body, $GLOBALS['didit_test_options']['didit_webhook_secret']), + ])); +} +foreach (['Declined', 'Expired', 'KYC Expired', 'In Review'] as $status) { + $record['event_timestamp'] = time() - 2; + $record['status'] = 'Approved'; + internal('store_session', $id, $record); + $GLOBALS['didit_test_user_meta']['42|_didit_session_id'] = $id; + $GLOBALS['didit_test_user_meta']['42|_didit_confirmed_session_id'] = $id; + $GLOBALS['didit_test_user_meta']['42|_didit_status'] = 'Approved'; + $GLOBALS['didit_test_user_meta']['42|_didit_verified'] = 1; + $GLOBALS['didit_test_user_meta']['42|_didit_pending_session_id'] = $pending_id; + signed_status($id, $status, time()); + check(!Didit_Verify::init()->is_user_verified(42), "$status revokes the current approval while another session is pending"); + check($status === internal('confirmed_user_status', 42), "$status is displayed on the user after revocation"); + check('' === get_user_meta(42, '_didit_verified', true), "$status clears the verified flag"); +} +$record['event_timestamp'] = 0; +$record['status'] = 'Not Started'; +internal('store_session', $pending_id, $record); +signed_status($pending_id, 'Approved', time()); +check(Didit_Verify::init()->is_user_verified(42), 'the new pending session can approve the user'); +signed_status($id, 'Declined', time() + 1); +check(Didit_Verify::init()->is_user_verified(42), 'a superseded session cannot revoke the new approval'); +check($pending_id === get_user_meta(42, '_didit_confirmed_session_id', true), 'a superseded session cannot replace the active session'); +signed_status($pending_id, 'Declined', time() - 1); +check(Didit_Verify::init()->is_user_verified(42), 'an older revocation cannot overwrite a newer approval'); +// Version 0.3.3 could already have recorded a revocation without updating user meta. +$revoked_record = internal('stored_session', $pending_id); +$revoked_record['status'] = 'Declined'; +internal('store_session', $pending_id, $revoked_record); +check(!Didit_Verify::init()->is_user_verified(42), 'an existing recorded revocation overrides stale approval metadata after upgrade'); +$revoked_record['status'] = 'Approved'; +$revoked_record['user_id'] = 7; +internal('store_session', $pending_id, $revoked_record); +check(!Didit_Verify::init()->is_user_verified(42), 'another user\'s session cannot supply the current approval'); echo "$passed passed\n";