diff --git a/languages/wp-ojs-sso-bridge-it_IT.po b/languages/wp-ojs-sso-bridge-it_IT.po index 4d1d59c..708d5eb 100644 --- a/languages/wp-ojs-sso-bridge-it_IT.po +++ b/languages/wp-ojs-sso-bridge-it_IT.po @@ -3,7 +3,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WP OJS SSO Bridge 1.2.0\n" +"Project-Id-Version: WP OJS SSO Bridge 1.2.1\n" "PO-Revision-Date: 2026-04-16T00:00:00+00:00\n" "Last-Translator: GuIT \n" "Language-Team: Italian\n" diff --git a/languages/wp-ojs-sso-bridge.pot b/languages/wp-ojs-sso-bridge.pot index 75cc7c7..1ba5f77 100644 --- a/languages/wp-ojs-sso-bridge.pot +++ b/languages/wp-ojs-sso-bridge.pot @@ -2,7 +2,7 @@ # This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: WP OJS SSO Bridge 1.2.0\n" +"Project-Id-Version: WP OJS SSO Bridge 1.2.1\n" "Report-Msgid-Bugs-To: https://github.com/nicola-arrigoni/wp-ojs-sso-bridge\n" "POT-Creation-Date: 2026-04-16T00:00:00+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" diff --git a/mu-plugins/README.txt b/mu-plugins/README.txt new file mode 100644 index 0000000..3e0fd0b --- /dev/null +++ b/mu-plugins/README.txt @@ -0,0 +1,27 @@ +Must-use plugins for WP OJS SSO Bridge +====================================== + +These files are NOT installed via wp-admin → Plugin → Upload. +Copy them manually into wp-content/mu-plugins/ on your WordPress site. + +Required for OJS SSO +-------------------- +guitex-oidc-well-known.php + Exposes JWKS and OpenID discovery (REST + optional /.well-known/). + Required because Automattic OpenID Connect Server does not publish JWKS. + +Optional +-------- +guitex-oidc-well-known.htaccess-snippet.txt + Apache rewrite snippet if /.well-known/ returns 404 before WordPress. + +Install +------- +mkdir -p wp-content/mu-plugins +cp mu-plugins/guitex-oidc-well-known.php wp-content/mu-plugins/ + +Verify (should return HTTP 200 JSON): + https://YOUR-SITE/wp-json/guitex/v1/jwks + https://YOUR-SITE/wp-json/guitex/v1/openid-configuration + +Requires OIDC_PUBLIC_KEY and OIDC_PRIVATE_KEY in wp-config.php. diff --git a/mu-plugins/guitex-oidc-well-known.htaccess-snippet.txt b/mu-plugins/guitex-oidc-well-known.htaccess-snippet.txt new file mode 100644 index 0000000..722a0c8 --- /dev/null +++ b/mu-plugins/guitex-oidc-well-known.htaccess-snippet.txt @@ -0,0 +1,9 @@ +# Opzionale: incolla PRIMA delle regole WordPress nel .htaccess della root di guitex.org +# Serve solo se vuoi usare /.well-known/* invece degli endpoint REST. +# Dopo aver salvato, in wp-admin: Impostazioni → Permalink → Salva (flush rewrite). + + +RewriteEngine On +RewriteRule ^\.well-known/jwks\.json$ /index.php?guitex_oidc_jwks=1 [L,QSA] +RewriteRule ^\.well-known/openid-configuration$ /index.php?guitex_oidc_discovery=1 [L,QSA] + diff --git a/mu-plugins/guitex-oidc-well-known.php b/mu-plugins/guitex-oidc-well-known.php new file mode 100644 index 0000000..576298a --- /dev/null +++ b/mu-plugins/guitex-oidc-well-known.php @@ -0,0 +1,325 @@ +query_vars[ GUITEX_OIDC_QV_JWKS ] ) ) { + guitex_oidc_well_known_send_jwks(); + } + if ( ! empty( $wp->query_vars[ GUITEX_OIDC_QV_DISCOVERY ] ) ) { + guitex_oidc_well_known_send_discovery(); + } +} + +/** + * @param array $vars Query vars. + * @return array + */ +function guitex_oidc_well_known_query_vars( $vars ) { + $vars[] = GUITEX_OIDC_QV_JWKS; + $vars[] = GUITEX_OIDC_QV_DISCOVERY; + return $vars; +} + +/** + * Rewrite opzionale: richiede salvataggio permalink (Impostazioni → Permalink → Salva). + * + * @return void + */ +function guitex_oidc_well_known_register_rewrites() { + add_rewrite_rule( + '^\.well-known/jwks\.json$', + 'index.php?' . GUITEX_OIDC_QV_JWKS . '=1', + 'top' + ); + add_rewrite_rule( + '^\.well-known/openid-configuration$', + 'index.php?' . GUITEX_OIDC_QV_DISCOVERY . '=1', + 'top' + ); +} + +/** + * Endpoint REST — funzionano anche quando il server blocca /.well-known/. + * + * @return void + */ +function guitex_oidc_well_known_register_rest_routes() { + register_rest_route( + 'guitex/v1', + '/jwks', + [ + 'methods' => 'GET', + 'callback' => 'guitex_oidc_well_known_rest_jwks', + 'permission_callback' => '__return_true', + ] + ); + + register_rest_route( + 'guitex/v1', + '/openid-configuration', + [ + 'methods' => 'GET', + 'callback' => 'guitex_oidc_well_known_rest_discovery', + 'permission_callback' => '__return_true', + ] + ); +} + +/** + * @return WP_REST_Response|WP_Error + */ +function guitex_oidc_well_known_rest_jwks() { + $payload = guitex_oidc_well_known_jwks_payload(); + if ( is_wp_error( $payload ) ) { + return $payload; + } + $response = new WP_REST_Response( $payload, 200 ); + $response->header( 'Cache-Control', 'public, max-age=3600' ); + return $response; +} + +/** + * @return WP_REST_Response + */ +function guitex_oidc_well_known_rest_discovery() { + $response = new WP_REST_Response( guitex_oidc_well_known_discovery_payload(), 200 ); + $response->header( 'Cache-Control', 'public, max-age=300' ); + return $response; +} + +/** + * @return 'jwks'|'discovery'|null + */ +function guitex_oidc_well_known_request_kind() { + $uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; + if ( $uri === '' ) { + return null; + } + + $path = (string) parse_url( $uri, PHP_URL_PATH ); + $path = rtrim( $path, '/' ); + + $home_path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); + $home_path = rtrim( $home_path, '/' ); + if ( $home_path !== '' && strpos( $path, $home_path ) === 0 ) { + $path = substr( $path, strlen( $home_path ) ); + if ( $path === '' ) { + $path = '/'; + } + } + + $suffixes = [ + '/.well-known/jwks.json' => 'jwks', + '/index.php/.well-known/jwks.json' => 'jwks', + '/.well-known/openid-configuration' => 'discovery', + '/index.php/.well-known/openid-configuration' => 'discovery', + ]; + + return $suffixes[ $path ] ?? null; +} + +/** + * @return void + */ +function guitex_oidc_well_known_send_jwks() { + $payload = guitex_oidc_well_known_jwks_payload(); + if ( is_wp_error( $payload ) ) { + guitex_oidc_well_known_json_error( $payload->get_error_message(), 503 ); + } + + $body = wp_json_encode( $payload, JSON_UNESCAPED_SLASHES ); + guitex_oidc_well_known_send_json( $body, 200, 3600 ); +} + +/** + * @return void + */ +function guitex_oidc_well_known_send_discovery() { + $body = wp_json_encode( guitex_oidc_well_known_discovery_payload(), JSON_UNESCAPED_SLASHES ); + guitex_oidc_well_known_send_json( $body, 200, 300 ); +} + +/** + * @return array{keys: array>}|WP_Error + */ +function guitex_oidc_well_known_jwks_payload() { + if ( ! defined( 'OIDC_PUBLIC_KEY' ) || OIDC_PUBLIC_KEY === '' ) { + return new WP_Error( + 'oidc_no_public_key', + 'OIDC_PUBLIC_KEY non definita in wp-config.php', + [ 'status' => 503 ] + ); + } + + $jwk = guitex_oidc_well_known_pem_to_jwk( OIDC_PUBLIC_KEY ); + if ( $jwk === null ) { + return new WP_Error( + 'oidc_invalid_public_key', + 'Impossibile leggere OIDC_PUBLIC_KEY (formato PEM non valido)', + [ 'status' => 503 ] + ); + } + + return [ 'keys' => [ $jwk ] ]; +} + +/** + * @return array + */ +function guitex_oidc_well_known_discovery_payload() { + $issuer = guitex_oidc_well_known_issuer(); + $jwks_uri = guitex_oidc_well_known_jwks_uri(); + + return [ + 'issuer' => $issuer, + 'authorization_endpoint' => rest_url( 'openid-connect/authorize' ), + 'token_endpoint' => rest_url( 'openid-connect/token' ), + 'userinfo_endpoint' => rest_url( 'openid-connect/userinfo' ), + 'jwks_uri' => $jwks_uri, + 'response_types_supported' => [ 'code' ], + 'grant_types_supported' => [ 'authorization_code' ], + 'subject_types_supported' => [ 'public' ], + 'id_token_signing_alg_values_supported' => [ 'RS256' ], + 'scopes_supported' => [ 'openid', 'profile', 'email' ], + ]; +} + +/** + * URI JWKS consigliata (REST se .well-known non raggiungibile). + * + * @return string + */ +function guitex_oidc_well_known_jwks_uri() { + return rest_url( 'guitex/v1/jwks' ); +} + +/** + * @param string $pem PEM public key. + * @return array|null + */ +function guitex_oidc_well_known_pem_to_jwk( $pem ) { + if ( ! function_exists( 'openssl_pkey_get_public' ) ) { + return null; + } + + $key = openssl_pkey_get_public( $pem ); + if ( $key === false ) { + return null; + } + + $details = openssl_pkey_get_details( $key ); + if ( ! is_array( $details ) || ( $details['type'] ?? null ) !== OPENSSL_KEYTYPE_RSA ) { + return null; + } + + $n = $details['rsa']['n'] ?? ''; + $e = $details['rsa']['e'] ?? ''; + if ( $n === '' || $e === '' ) { + return null; + } + + $n_b64 = guitex_oidc_well_known_base64url( $n ); + + return [ + 'kty' => 'RSA', + 'alg' => 'RS256', + 'use' => 'sig', + 'kid' => substr( hash( 'sha256', $n_b64 ), 0, 16 ), + 'n' => $n_b64, + 'e' => guitex_oidc_well_known_base64url( $e ), + ]; +} + +/** + * @param string $data Raw binary. + * @return string + */ +function guitex_oidc_well_known_base64url( $data ) { + return rtrim( strtr( base64_encode( $data ), '+/', '-_' ), '=' ); +} + +/** + * @return string + */ +function guitex_oidc_well_known_issuer() { + return rtrim( home_url(), '/' ); +} + +/** + * @param string $body JSON. + * @param int $status HTTP status. + * @param int $max_age Cache seconds. + * @return void + */ +function guitex_oidc_well_known_send_json( $body, $status, $max_age ) { + if ( ! headers_sent() ) { + status_header( $status ); + header( 'Content-Type: application/json; charset=utf-8' ); + header( 'X-Robots-Tag: noindex' ); + header( $max_age > 0 ? 'Cache-Control: public, max-age=' . (int) $max_age : 'Cache-Control: no-store' ); + } + + echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; +} + +/** + * @param string $message Error text. + * @param int $status HTTP status. + * @return void + */ +function guitex_oidc_well_known_json_error( $message, $status ) { + $body = wp_json_encode( + [ + 'error' => 'configuration_error', + 'error_description' => $message, + ], + JSON_UNESCAPED_SLASHES + ); + guitex_oidc_well_known_send_json( $body, $status, 0 ); +} diff --git a/readme.txt b/readme.txt index 5fc3c2c..7f70692 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Requires at least: 6.0 Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 1.2.0 +Stable tag: 1.2.1 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -29,13 +29,20 @@ == Installation == 1. Install and activate the "OpenID Connect Server" plugin by Automattic. -2. Upload the `wp-ojs-sso-bridge` folder to `/wp-content/plugins/`. -3. Activate the plugin through the 'Plugins' menu. -4. Go to Settings > OJS SSO Bridge and configure your OJS client credentials. -5. Generate RSA keys for the OIDC server: `wp eval 'OIDC\generateKeys();'` +2. Install this plugin: + * **Release ZIP** (wp-admin → Plugins → Add New → Upload): installs only the main plugin under `/wp-content/plugins/`. + * **Git clone / manual copy**: upload the `wp-ojs-sso-bridge` folder to `/wp-content/plugins/`. +3. **Must-use plugin (required for OJS):** copy `mu-plugins/guitex-oidc-well-known.php` from this repository into `/wp-content/mu-plugins/` (not included in the release ZIP). See `mu-plugins/README.txt`. +4. Activate the plugin through the 'Plugins' menu. +5. Go to Settings > OJS SSO Bridge and configure your OJS client credentials. +6. Generate RSA keys for the OIDC server: `wp eval 'OIDC\generateKeys();'` == Changelog == += 1.2.1 = +* MU-plugin `guitex-oidc-well-known.php` (JWKS + OpenID discovery) included in the repository under `mu-plugins/`; install manually to `wp-content/mu-plugins/`. +* Release ZIP unchanged: still contains only the main plugin (mu-plugins are excluded from upload). + = 1.2.0 = * Quick-filter links on the Users screen: "Subscription requested" and "Active subscription" (with counts). * Removed the hard-to-find subscription dropdown above the users table. diff --git a/wp-ojs-sso-bridge.php b/wp-ojs-sso-bridge.php index aff5dd5..eaf9bc6 100644 --- a/wp-ojs-sso-bridge.php +++ b/wp-ojs-sso-bridge.php @@ -3,7 +3,7 @@ * Plugin Name: WP OJS SSO Bridge * Plugin URI: https://github.com/nicola-arrigoni/wp-ojs-sso-bridge * Description: Bridges WordPress and Open Journal Systems (OJS) via OpenID Connect. Manages journal subscription status on WordPress users and exposes it as an OIDC claim so OJS can grant or deny access to protected issues. - * Version: 1.2.0 + * Version: 1.2.1 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: GuIT – Gruppo Utilizzatori Italiani di TeX @@ -18,7 +18,7 @@ exit; } -define( 'WP_OJS_SSO_VERSION', '1.2.0' ); +define( 'WP_OJS_SSO_VERSION', '1.2.1' ); define( 'WP_OJS_SSO_FILE', __FILE__ ); define( 'WP_OJS_SSO_DIR', plugin_dir_path( __FILE__ ) ); define( 'WP_OJS_SSO_URL', plugin_dir_url( __FILE__ ) );