Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .distignore
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 26 additions & 40 deletions readme.txt
Original file line number Diff line number Diff line change
@@ -1,58 +1,44 @@
=== Copyright Date Block ===
Contributors: Credenco B.V.
Tags: block
Tested up to: 6.6
=== Universal OID4VP ===
Contributors: credenco
Tags: openid4vp, verifiable-credentials, sso, login, identity
Requires at least: 6.6
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

Display your site's copyright date.
Retrieve verifiable presentations from a personal or business 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 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.

= 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.
4 changes: 2 additions & 2 deletions src/OpenID4VP.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/adminSettings/openid4vp-admin-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public function universal_openid4vp_create_settings_page() {
</div>
<hr />
<p class="submit">
<input type="submit" class="button-primary" value="<?php esc_html_e('Save Changes', 'universal-openid4vp-plugin') ?>"/>
<input type="submit" class="button-primary" value="<?php esc_html_e('Save Changes', 'universal-openid4vp') ?>"/>
</p>
</form>
</div>
Expand Down
96 changes: 96 additions & 0 deletions src/openid4vp-session.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

if ( ! defined( 'UNIVERSAL_OPENID4VP_COOKIE_NAME' ) ) {
define( 'UNIVERSAL_OPENID4VP_COOKIE_NAME', 'universal_openid4vp_session' );
}
if ( ! defined( 'UNIVERSAL_OPENID4VP_TRANSIENT_PREFIX' ) ) {
define( 'UNIVERSAL_OPENID4VP_TRANSIENT_PREFIX', 'uo4vp_' );
}
if ( ! defined( 'UNIVERSAL_OPENID4VP_SESSION_TTL' ) ) {
define( 'UNIVERSAL_OPENID4VP_SESSION_TTL', HOUR_IN_SECONDS );
}

function universal_openid4vp_session_bootstrap() {
if ( ! empty( $_COOKIE[ UNIVERSAL_OPENID4VP_COOKIE_NAME ] ) ) {
return;
}

if ( headers_sent() ) {
return;
}

try {
$token = bin2hex( random_bytes( 16 ) );
} catch ( Exception $e ) {
$token = wp_generate_password( 32, false, false );
}

$_COOKIE[ UNIVERSAL_OPENID4VP_COOKIE_NAME ] = $token;

setcookie(
UNIVERSAL_OPENID4VP_COOKIE_NAME,
$token,
array(
'expires' => 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 );
}
2 changes: 1 addition & 1 deletion src/presentationAttribute/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"fontSize": true
}
},
"textdomain": "openid4vp-attribute",
"textdomain": "universal-openid4vp",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
Expand Down
8 changes: 4 additions & 4 deletions src/presentationAttribute/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export default function Edit( { attributes, setAttributes } ) {
return (
<>
<InspectorControls>
<PanelBody title={__('Settings', 'openid4vp-attribute')}>
<PanelBody title={__('Settings', 'universal-openid4vp')}>
<TextControl
label={ __(
'Credential query id',
'openid4vp-attribute'
'universal-openid4vp'
) }
value={ credentialQueryId }
onChange={ ( value ) =>
Expand All @@ -68,7 +68,7 @@ export default function Edit( { attributes, setAttributes } ) {
<TextControl
label={ __(
'VP attribute label',
'openid4vp-attribute'
'universal-openid4vp'
) }
value={ attributeLabel }
onChange={ ( value ) =>
Expand All @@ -78,7 +78,7 @@ export default function Edit( { attributes, setAttributes } ) {
<TextControl
label={ __(
'VP attribute name',
'openid4vp-attribute'
'universal-openid4vp'
) }
value={ attributeName }
onChange={ ( value ) =>
Expand Down
58 changes: 24 additions & 34 deletions src/presentationAttribute/render.php
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
<?php
/**
* PHP file to use when rendering the block type on the server to show on the front end.
*
* The following variables are exposed to the file:
* $attributes (array): The block attributes.
* $content (string): The block default content.
* $block (WP_Block): The block instance.
*
* @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();
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

// 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(
Expand All @@ -33,24 +24,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' );
}
}

Expand All @@ -73,13 +64,12 @@
// $arr is now array(2, 4, 6, 8)
unset($name);

$label = trim($attributes['attributeLabel']);
if ($label !== '') {
$block_content = '<p ' . get_block_wrapper_attributes() . '>' . esc_html($label) . ': ' . esc_html($result) . '</p>';
} else {
$block_content = '<p ' . get_block_wrapper_attributes() . '>' . esc_html($result) . '</p>';
$label = isset( $attributes['attributeLabel'] ) ? trim( $attributes['attributeLabel'] ) : '';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() returns core-sanitized HTML attributes.
echo '<p ' . get_block_wrapper_attributes() . '>';
if ( '' !== $label ) {
echo esc_html( $label ) . ': ';
}

echo $block_content;
echo esc_html( $result ) . '</p>';
}
}
Loading