From 7b221ddc3fce43ed7bbecc780315e6dd3d530b7c Mon Sep 17 00:00:00 2001
From: mdebruijn
Date: Wed, 22 Apr 2026 12:40:15 +0200
Subject: [PATCH 1/3] Harden plugin for wordpress.org submission
- Rewrite readme.txt with real plugin metadata and external-services disclosure.
- Replace PHP sessions with a cookie-scoped token + WP transient store
(new src/openid4vp-session.php); drop every session_start / $_SESSION site.
- Add nonce verification and input sanitization on both AJAX handlers;
send the nonce from pollStatus.js and submitPresentationRequest.js.
- Escape output in render.php files; switch json_encode to wp_json_encode;
wrap error HTML in wp_kses_post; escape API-supplied detail strings.
- Remove error_log() calls that dumped verified presentation bodies.
- Drop the bogus root register_block_type(__DIR__, ...) init hook.
- Delete placeholder status.php and login.php (unused, generic prefixes).
- Bump Requires PHP to 7.3 (needed for setcookie options array / SameSite).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
readme.txt | 64 +++++--------
src/openid4vp-session.php | 96 +++++++++++++++++++
src/presentationAttribute/render.php | 35 ++++---
src/presentationExchange/login.php | 16 ----
src/presentationExchange/pollStatus.js | 1 +
src/presentationExchange/render.php | 14 ++-
src/presentationExchange/status.php | 26 -----
src/presentationExchangeOrgWallet/render.php | 15 +--
.../submitPresentationRequest.js | 1 +
universal-openid4vp-plugin.php | 81 ++++++++--------
10 files changed, 191 insertions(+), 158 deletions(-)
create mode 100644 src/openid4vp-session.php
delete mode 100644 src/presentationExchange/login.php
delete mode 100644 src/presentationExchange/status.php
diff --git a/readme.txt b/readme.txt
index 9d557e1..55446b6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,58 +1,44 @@
-=== Copyright Date Block ===
-Contributors: Credenco B.V.
-Tags: block
+=== Universal OID4VP ===
+Contributors: credenco
+Tags: openid4vp, verifiable-credentials, sso, login, identity
+Requires at least: 6.6
Tested up to: 6.6
+Requires PHP: 7.3
Stable tag: 0.7.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
-Display your site's copyright date.
+Retrieve verifiable presentations from a personal or organizational wallet using the OpenID for Verifiable Presentations (OpenID4VP) flow.
== Description ==
-This is the long description. No limit, and you can use Markdown (as well as in the following sections).
+Universal OID4VP lets a WordPress site request and consume verifiable credentials from a user's digital wallet over the OpenID for Verifiable Presentations (OpenID4VP) protocol.
-For backwards compatibility, if this section is missing, the full length of the short description will be used, and
-Markdown parsed.
+The plugin adds:
-== Installation ==
-
-This section describes how to install the plugin and get it working.
-
-e.g.
-
-1. Upload the plugin files to the `/wp-content/plugins/openid4vp-block` directory, or install the plugin through the WordPress plugins screen directly.
-1. Activate the plugin through the 'Plugins' screen in WordPress
-
-
-== Frequently Asked Questions ==
+* A settings page where the site administrator configures the OpenID4VP and token endpoints, API client credentials, and optional wallet-based login behaviour.
+* A block that starts a presentation exchange with a personal wallet (QR code or link).
+* A block that starts a presentation exchange with an organizational wallet.
+* A block that displays an attribute obtained from a received presentation.
+* An optional "Login with Personal Wallet" button on the standard WordPress login form.
-= A question that someone might have =
+== External services ==
-An answer to that question.
+This plugin communicates with an OpenID4VP backend that you configure in the plugin settings (for example `https://wallet.acc.credenco.com`) and with a token endpoint (for example a Keycloak `/protocol/openid-connect/token` URL). Both URLs are chosen by the site administrator. No data is sent to those services until a visitor actively uses a wallet block or the wallet login button.
-= What about foo bar? =
+The data exchanged with the configured endpoints includes the API client credentials entered on the settings page, the query identifier configured on the block, and — for responses — the verifiable presentation returned by the wallet.
-Answer to foo bar dilemma.
-
-== Screenshots ==
+== Installation ==
-1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from
-the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets
-directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png`
-(or jpg, jpeg, gif).
-2. This is the second screen shot
+1. Upload the plugin zip via the "Plugins" screen in WordPress, or extract it to `/wp-content/plugins/universal-openid4vp-plugin`.
+2. Activate the plugin through the "Plugins" screen.
+3. Go to Settings -> Universal OID4VP and fill in the OpenID4VP endpoint, token endpoint, API client id and secret.
+4. Add one of the Universal OID4VP blocks to a page to start a presentation exchange.
== Changelog ==
-= 0.1.0 =
-* Release
-
-= 0.2.0 =
-* Release
-
-== Arbitrary section ==
+= 0.7.0 =
+* Maintenance release: hardening for wordpress.org submission (input sanitization, output escaping, nonces on AJAX endpoints, readme).
-You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated
-plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or
-"installation." Arbitrary sections will be shown below the built-in sections outlined above.
+= 0.1.0 =
+* Initial release.
diff --git a/src/openid4vp-session.php b/src/openid4vp-session.php
new file mode 100644
index 0000000..d3668f7
--- /dev/null
+++ b/src/openid4vp-session.php
@@ -0,0 +1,96 @@
+ 0,
+ 'path' => defined( 'COOKIEPATH' ) && COOKIEPATH ? COOKIEPATH : '/',
+ 'domain' => defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '',
+ 'secure' => is_ssl(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ )
+ );
+}
+add_action( 'init', 'universal_openid4vp_session_bootstrap', 0 );
+
+function universal_openid4vp_session_token() {
+ if ( empty( $_COOKIE[ UNIVERSAL_OPENID4VP_COOKIE_NAME ] ) ) {
+ return '';
+ }
+ $token = sanitize_text_field( wp_unslash( $_COOKIE[ UNIVERSAL_OPENID4VP_COOKIE_NAME ] ) );
+ if ( ! preg_match( '/^[A-Za-z0-9]{16,64}$/', $token ) ) {
+ return '';
+ }
+ return $token;
+}
+
+function universal_openid4vp_session_key() {
+ $token = universal_openid4vp_session_token();
+ return $token ? UNIVERSAL_OPENID4VP_TRANSIENT_PREFIX . $token : '';
+}
+
+function universal_openid4vp_session_get_all() {
+ $key = universal_openid4vp_session_key();
+ if ( empty( $key ) ) {
+ return array();
+ }
+ $data = get_transient( $key );
+ return is_array( $data ) ? $data : array();
+}
+
+function universal_openid4vp_session_get( $name, $default_value = null ) {
+ $data = universal_openid4vp_session_get_all();
+ return isset( $data[ $name ] ) ? $data[ $name ] : $default_value;
+}
+
+function universal_openid4vp_session_set( $name, $value ) {
+ $key = universal_openid4vp_session_key();
+ if ( empty( $key ) ) {
+ return false;
+ }
+ $data = universal_openid4vp_session_get_all();
+ $data[ $name ] = $value;
+ return set_transient( $key, $data, UNIVERSAL_OPENID4VP_SESSION_TTL );
+}
+
+function universal_openid4vp_session_delete( $name ) {
+ $key = universal_openid4vp_session_key();
+ if ( empty( $key ) ) {
+ return false;
+ }
+ $data = universal_openid4vp_session_get_all();
+ unset( $data[ $name ] );
+ return set_transient( $key, $data, UNIVERSAL_OPENID4VP_SESSION_TTL );
+}
diff --git a/src/presentationAttribute/render.php b/src/presentationAttribute/render.php
index 3fe2fb2..9692f38 100644
--- a/src/presentationAttribute/render.php
+++ b/src/presentationAttribute/render.php
@@ -9,19 +9,16 @@
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/
-// do a session a start
-if (session_status() === PHP_SESSION_NONE) {
- session_start();
-}
-
// Retrieve the presentation response
-$presentationResponse = isset($_SESSION['presentationResponse']) ? $_SESSION['presentationResponse'] : null;
-$presentationStatusUri = isset($_SESSION['presentationStatusUri']) ? $_SESSION['presentationStatusUri'] : null;
+$presentationResponse = universal_openid4vp_session_get( 'presentationResponse' );
+$presentationStatusUri = universal_openid4vp_session_get( 'presentationStatusUri' );
+$storedSuccessUrl = universal_openid4vp_session_get( 'successUrl' );
+$storedAccessToken = universal_openid4vp_session_get( 'accessToken' );
-if (!empty($_SESSION['successUrl']) && !empty($presentationStatusUri)) {
+if ( ! empty( $storedSuccessUrl ) && ! empty( $presentationStatusUri ) ) {
$headers = array('Content-Type' => 'application/json');
- if (isset($_SESSION['accessToken'])) {
- $headers['Authorization'] = 'Bearer ' . $_SESSION['accessToken'];
+ if ( ! empty( $storedAccessToken ) ) {
+ $headers['Authorization'] = 'Bearer ' . $storedAccessToken;
}
$response = wp_remote_get( $presentationStatusUri, array(
@@ -33,24 +30,24 @@
$body = wp_remote_retrieve_body($response);
- error_log('Result: '. $body);
-
$successUrl = null;
$statusResponse = json_decode( $body, true);
if ( $statusResponse['status'] === 'authorization_response_verified' ) {
$credentialClaims = $statusResponse['verified_data']['credential_claims'];
+ $storedPresentationResponse = universal_openid4vp_session_get( 'presentationResponse', array() );
+ if ( ! is_array( $storedPresentationResponse ) ) {
+ $storedPresentationResponse = array();
+ }
foreach ($credentialClaims as $credential) {
- if (empty($_SESSION['presentationResponse'])) {
- $_SESSION['presentationResponse'] = [];
- }
- $_SESSION['presentationResponse'][$credential['id']] = $credential;
+ $storedPresentationResponse[ $credential['id'] ] = $credential;
}
- $presentationResponse = isset($_SESSION['presentationResponse']) ? $_SESSION['presentationResponse'] : null;
+ universal_openid4vp_session_set( 'presentationResponse', $storedPresentationResponse );
+ $presentationResponse = $storedPresentationResponse;
- $_SESSION['accessToken'] = null;
- $_SESSION['successUrl'] = null;
+ universal_openid4vp_session_delete( 'accessToken' );
+ universal_openid4vp_session_delete( 'successUrl' );
}
}
diff --git a/src/presentationExchange/login.php b/src/presentationExchange/login.php
deleted file mode 100644
index d723140..0000000
--- a/src/presentationExchange/login.php
+++ /dev/null
@@ -1,16 +0,0 @@
-qr_uri . '">>or ' : '';
-$block_content = '
';
From 9a3b42f2c7403d633ced7093c0d9072b6ac00fb3 Mon Sep 17 00:00:00 2001
From: mdebruijn
Date: Wed, 22 Apr 2026 16:39:30 +0200
Subject: [PATCH 3/3] Pass wordpress.org Plugin Check (0 errors)
- Add ABSPATH guards to all render.php files
- Unify text domain to universal-openid4vp across plugin header,
admin settings, block.json files, and render/edit sources
- Bump Tested up to 6.9 and Requires PHP 7.4 in readme.txt and
plugin header
- Escape render.php output per-part; document the two phpcs:ignore
cases where get_block_wrapper_attributes() is echoed (core-sanitized)
- Switch wp_redirect() to wp_safe_redirect() on logout
- Add version + in_footer to wp_enqueue_script calls using a new
UNIVERSAL_OPENID4VP_PLUGIN_VERSION constant
- Rename the misnamed wp_enqueue_script custom hook to a prefixed
universal_openid4vp_enqueue_personal_wallet_scripts_action; drop
the unused submitPresentationRequest do_action call
- Document nonce verification on $_POST['walletUrl'] (verified
upstream in the AJAX handler) with phpcs:ignore
- Strip commented-out scaffold from pollStatus.js; only poll again
when the response has no successUrl
- Accessible org-wallet form: label, type="url", required; mirror
in edit.js (also swap invalid
wrapper for
)
- Drop the dead auth-header/token override path in org-wallet render
- Rename "organizational wallet" to "business wallet" in block
title/description and readme prose
- Add .distignore so the plugin zip excludes .git, .idea, src,
node_modules, package*.json, tests, local artifacts
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.distignore | 43 ++++++++++++
readme.txt | 8 +--
src/OpenID4VP.php | 4 +-
.../openid4vp-admin-settings.php | 2 +-
src/presentationAttribute/block.json | 2 +-
src/presentationAttribute/edit.js | 8 +--
src/presentationAttribute/render.php | 27 +++-----
src/presentationExchange/block.json | 2 +-
src/presentationExchange/edit.js | 26 +++----
src/presentationExchange/pollStatus.js | 68 ++++++-------------
src/presentationExchange/render.php | 22 +++---
src/presentationExchangeOrgWallet/block.json | 6 +-
src/presentationExchangeOrgWallet/edit.js | 33 +++++----
src/presentationExchangeOrgWallet/render.php | 49 ++++---------
universal-openid4vp-plugin.php | 36 +++++++---
15 files changed, 179 insertions(+), 157 deletions(-)
create mode 100644 .distignore
diff --git a/.distignore b/.distignore
new file mode 100644
index 0000000..9a90621
--- /dev/null
+++ b/.distignore
@@ -0,0 +1,43 @@
+# Files and directories that must not ship inside the plugin zip.
+# `@wordpress/scripts plugin-zip` and wordpress.org's distribution tools
+# respect these ignore rules.
+
+# VCS and editor metadata
+.git
+.github
+.gitignore
+.gitattributes
+.idea
+.vscode
+.DS_Store
+
+# Node / build tooling
+node_modules
+package.json
+package-lock.json
+yarn.lock
+webpack.config.js
+.wp-env.json
+
+# Source (only build/ ships)
+src
+
+# Tests / CI
+tests
+phpunit.xml
+phpunit.xml.dist
+.phpcs.xml
+.phpcs.xml.dist
+phpcs.xml
+phpcs.xml.dist
+
+# Local artifacts
+.playwright-mcp
+*.png
+*.log
+*.map
+
+# Markdown readme (readme.txt is the canonical one)
+readme.md
+README.md
+CHANGELOG.md
diff --git a/readme.txt b/readme.txt
index 55446b6..2733d97 100644
--- a/readme.txt
+++ b/readme.txt
@@ -2,13 +2,13 @@
Contributors: credenco
Tags: openid4vp, verifiable-credentials, sso, login, identity
Requires at least: 6.6
-Tested up to: 6.6
-Requires PHP: 7.3
+Tested up to: 6.9
+Requires PHP: 7.4
Stable tag: 0.7.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
-Retrieve verifiable presentations from a personal or organizational wallet using the OpenID for Verifiable Presentations (OpenID4VP) flow.
+Retrieve verifiable presentations from a personal or business wallet using the OpenID for Verifiable Presentations (OpenID4VP) flow.
== Description ==
@@ -18,7 +18,7 @@ The plugin adds:
* A settings page where the site administrator configures the OpenID4VP and token endpoints, API client credentials, and optional wallet-based login behaviour.
* A block that starts a presentation exchange with a personal wallet (QR code or link).
-* A block that starts a presentation exchange with an organizational wallet.
+* A block that starts a presentation exchange with a business wallet.
* A block that displays an attribute obtained from a received presentation.
* An optional "Login with Personal Wallet" button on the standard WordPress login form.
diff --git a/src/OpenID4VP.php b/src/OpenID4VP.php
index 7f7f935..2dac561 100644
--- a/src/OpenID4VP.php
+++ b/src/OpenID4VP.php
@@ -45,8 +45,8 @@ public function setup() {
}
public function logout() {
- wp_redirect(home_url());
- exit();
+ wp_safe_redirect( home_url() );
+ exit;
}
public function wp_enqueue() {
diff --git a/src/adminSettings/openid4vp-admin-settings.php b/src/adminSettings/openid4vp-admin-settings.php
index a423e97..0a6524f 100644
--- a/src/adminSettings/openid4vp-admin-settings.php
+++ b/src/adminSettings/openid4vp-admin-settings.php
@@ -114,7 +114,7 @@ public function universal_openid4vp_create_settings_page() {