sto configurando DiscourseConnect tra un sito WordPress e un’istanza Discourse ospitata e sto riscontrando un persistente errore di validazione della firma HMAC.
WordPress riceve la richiesta DiscourseConnect e restituisce il callback contenente sso e sig.
Il callback raggiunge Discourse, ma Discourse lo rifiuta perché la firma HMAC non viene validata.
Abbiamo effettuato un debug piuttosto esteso e abbiamo ridotto notevolmente il campo di ricerca.
Cosa abbiamo confermato
wp_unslash() non altera i valori del callback.
Gli input di validazione EA/WordPress corrispondono ai parametri ricostruiti dalla stringa di query osservata da WordPress.
Il codice del callback previsto viene sicuramente eseguito.
I file sorgente distribuiti corrispondono al nostro manifesto di build.
Abbiamo verificato la presenza di file errati o definizioni duplicate.
PHP-FPM è stato riavviato dopo le ultime correzioni, quindi non si tratta di uno stato obsoleto di un processo PHP/opcache.
Una richiesta DiscourseConnect completamente nuova dopo tali modifiche fallisce ancora la validazione HMAC.
In altre parole, l’errore attuale è riproducibile su una richiesta nuova.
Cosa non siamo ancora riusciti a dimostrare
Che il segreto effettivo utilizzato da Discourse in tempo di esecuzione sia identico, byte per byte, al segreto utilizzato da WordPress.
Che il payload sso non venga modificato da qualche parte prima del punto in cui WordPress osserva la richiesta.
A questo punto non vogliamo continuare a modificare impostazioni o codice alla cieca.
Domanda
Per l’attuale versione di Discourse/DiscourseConnect, qual è il modo migliore per determinare esattamente quale payload e quale segreto Discourse sta utilizzando quando calcola l’HMAC atteso?
Esiste un metodo di debug/logging consigliato che ci permetterebbe di confrontare l’input HMAC lato Discourse con l’input lato WordPress senza esporre pubblicamente il segreto effettivo?
Se ci sono problemi noti che coinvolgono la gestione di WordPress/PHP, la codifica URL, i payload Base64, i proxy inversi o Discourse ospitato che potrebbero causare questa situazione, apprezzerei anche degli indizi.
Posso fornire valori di richiesta/callback sanitizzati, codice WordPress rilevante e log se necessario.
Ciao Richard. WP-Discourse è installato, ma il provider DiscourseConnect e le funzioni di sincronizzazione del login sono disabilitati.
Stiamo utilizzando un piccolo plugin WordPress personalizzato come provider DiscourseConnect. Esso riceve i parametri sso e sig da Discourse, verifica l’HMAC in ingresso utilizzando il segreto condiviso e quindi costruisce/firma la risposta da inviare a Discourse.
L’errore si verifica attualmente sulla richiesta in ingresso da Discourse a WordPress — la nostra nuova diagnostica mostra che il ricalcolo dell’HMAC non corrisponde al sig ricevuto da Discourse.
Sono felice di pubblicare il codice PHP del callback/validazione pertinente. Rimuoverò i valori di configurazione/segreti prima di pubblicarlo.
La via più ovvia sarebbe quella di utilizzare il plugin WP-Discourse, ma probabilmente hai buone ragioni per non farlo.
Sì, per favore pubblica il codice.
Sì — hai ragione. La formulazione nel mio post originale era invertita.
Il fallimento attuale avviene nella direzione Discourse → WordPress.
Discourse genera la richiesta DiscourseConnect contenente sso e sig. WordPress la riceve e il nostro provider EA personalizzato tenta di verificare la firma. La verifica HMAC in ingresso fallisce, quindi WordPress si ferma lì. Non arriva nemmeno al punto di restituire il payload dell’utente autenticato a Discourse.
WP-Discourse è installato, ma il suo provider DiscourseConnect e le funzioni di sincronizzazione dell’accesso utente sono disabilitati. Attualmente utilizziamo il provider EA personalizzato perché vogliamo che il livello di account/profilo EA rimanga sotto il nostro controllo.
Di seguito pubblicherò il codice rilevante per callback/configurazione/validazione HMAC, con tutti i segreti e le configurazioni private rimossi.
Questo è il percorso di validazione standard in ingresso, sanificato, di DiscourseConnect (Discourse → WordPress) nel provider WordPress personalizzato di EA.
<?php
// Le costanti di configurazione sono definite altrove; i loro valori privati sono omessi.
final class EA_Discourse_Connect {
public function __construct() {
add_action( 'admin_post_ea_discourse_connect', array( $this, 'connect' ) );
add_action( 'admin_post_nopriv_ea_discourse_connect', array( $this, 'connect' ) );
}
public static function enabled() {
return defined( 'EA_DISCOURSE_CONNECT_ENABLED' ) && true === EA_DISCOURSE_CONNECT_ENABLED;
}
public static function config() {
$url = defined( 'EA_DISCOURSE_URL' ) ? EA_DISCOURSE_URL : '';
$secret = defined( 'EA_DISCOURSE_CONNECT_SECRET' ) ? EA_DISCOURSE_CONNECT_SECRET : '';
if ( ! is_string( $url ) || ! preg_match( '~\Ahttps://[a-z0-9.-]+(?::[0-9]+)?(?:/[a-z0-9_-]+)*/?\z~i', $url ) ) {
$url = '';
}
return array(
'url' => rtrim( $url, '/' ),
'secret' => is_string( $secret ) ? $secret : '',
);
}
public static function ready() {
$c = self::config();
return self::enabled()
&& $c['url']
&& strlen( $c['secret'] ) >= 32
&& 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME );
}
private function fail( $message, $code = 400 ) {
wp_die(
esc_html( $message ),
'EA community sign-in',
array( 'response' => $code )
);
}
public static function validate( $payload, $signature, $config ) {
if (
! is_string( $payload )
|| ! is_string( $signature )
|| strlen( $payload ) > 8192
|| ! preg_match( '/\A[a-f0-9]{64}\z/', $signature )
|| ! hash_equals(
hash_hmac( 'sha256', $payload, $config['secret'] ),
$signature
)
) {
return false;
}
$decoded = base64_decode( $payload, true );
if ( false === $decoded ) {
return false;
}
parse_str( $decoded, $params );
if (
empty( $params['nonce'] )
|| ! is_string( $params['nonce'] )
|| strlen( $params['nonce'] ) > 256
|| ! isset( $params['return_sso_url'] )
|| $config['url'] . '/session/sso_login' !== $params['return_sso_url']
) {
return false;
}
return $params['nonce'];
}
public function connect() {
// Configurazione degli header di risposta omessa.
if ( ! self::enabled() ) {
$this->fail( 'Community sign-in is disabled.', 503 );
}
if ( ! self::ready() ) {
$this->fail( 'Community sign-in is not configured.', 503 );
}
$c = self::config();
$payload = isset( $_GET['sso'] )
? wp_unslash( $_GET['sso'] )
: null;
$sig = isset( $_GET['sig'] )
? wp_unslash( $_GET['sig'] )
: null;
$nonce = self::validate( $payload, $sig, $c );
if ( false === $nonce ) {
$this->fail(
'Invalid community sign-in request. Start again from the community.'
);
}
// Login successivo di WordPress e codice di risposta autenticato omessi.
}
}
// Istanziata dal bootstrap del plugin:
new EA_Discourse_Connect();
Grazie, Richard. Ho verificato quel punto rispetto alla gestione delle richieste in PHP/WordPress e al flusso di firma corrente di Discourse.
Nel nostro callback leggiamo da $_GET, quindi PHP ha già decodificato i parametri della query prima che EA li riceva. wp_unslash() si limita a invertire la slash escaping delle richieste di WordPress; non esegue la decodifica URL.
Applicare nuovamente urldecode() a quel punto causerebbe una doppia decodifica del valore e potrebbe trasformare un + Base64 in uno spazio, il che a sua volta romperebbe l’HMAC.
Ho anche confermato che la nostra nuova diagnostica fallisce ancora l’HMAC dopo che i segreti di WordPress e Discourse sono stati corretti per corrispondere.
Quindi non credo che sostituire wp_unslash() con urldecode() sia la soluzione giusta per questo particolare percorso del callback. Continuo a tracciare dove il payload firmato potrebbe divergere prima di raggiungere PHP.