<?php
/**
* Plugin Name: GuIT OIDC Well-Known (JWKS + Discovery)
* Description: Espone JWKS e OpenID discovery per OJS (pkp/openid) da OIDC_PUBLIC_KEY. Route REST (affidabile) + .well-known (se il rewrite passa da WordPress).
* Version: 1.1.0
* Author: GuIT
*
* Installazione: wp-content/mu-plugins/guitex-oidc-well-known.php
* Richiede OIDC_PUBLIC_KEY in wp-config.php (generate-oidc-keys.php).
*
* Se /.well-known/* restituisce 404 nginx/apache (non WordPress), usa in OJS:
* certUrl: https://TUO-DOMINIO/wp-json/guitex/v1/jwks
* configUrl: https://TUO-DOMINIO/wp-json/guitex/v1/openid-configuration
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** Query vars per rewrite .well-known → index.php */
const GUITEX_OIDC_QV_JWKS = 'guitex_oidc_jwks';
const GUITEX_OIDC_QV_DISCOVERY = 'guitex_oidc_discovery';
add_action( 'plugins_loaded', 'guitex_oidc_well_known_maybe_serve_path', 0 );
add_filter( 'query_vars', 'guitex_oidc_well_known_query_vars' );
add_action( 'init', 'guitex_oidc_well_known_register_rewrites', 0 );
add_action( 'parse_request', 'guitex_oidc_well_known_parse_request', 0 );
add_action( 'rest_api_init', 'guitex_oidc_well_known_register_rest_routes' );
/**
* Intercetta path /.well-known/* quando la richiesta arriva a WordPress.
*
* @return void
*/
function guitex_oidc_well_known_maybe_serve_path() {
$kind = guitex_oidc_well_known_request_kind();
if ( $kind === 'jwks' ) {
guitex_oidc_well_known_send_jwks();
}
if ( $kind === 'discovery' ) {
guitex_oidc_well_known_send_discovery();
}
}
/**
* @param WP $wp WordPress request object.
* @return void
*/
function guitex_oidc_well_known_parse_request( $wp ) {
if ( ! empty( $wp->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<int, string> $vars Query vars.
* @return array<int, string>
*/
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<int, array<string, string>>}|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<string, mixed>
*/
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<string, string>|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 );
}