I’m setting up DiscourseConnect between a WordPress site and a hosted Discourse instance and have reached a persistent HMAC signature validation failure.
WordPress receives the DiscourseConnect request and returns the callback containing sso and sig.
The callback reaches Discourse, but Discourse rejects it because the HMAC signature does not validate.
We have done fairly extensive debugging and have narrowed the problem down considerably.
What we have confirmed
wp_unslash() does not alter the callback values.
The EA/WordPress validation inputs match the parameters reconstructed from WordPress’s observed query string.
The expected callback code is definitely executing.
The deployed source files match our build manifest.
We have checked for wrong-file and duplicate-definition issues.
PHP-FPM has been restarted after the latest corrections, so this is not an old PHP process/opcache state.
A completely fresh DiscourseConnect request after those changes still fails HMAC validation.
In other words, the current failure is reproducible on a fresh request.
What we have not yet been able to prove
That the effective secret being used by Discourse at runtime is byte-for-byte identical to the secret being used by WordPress.
That the sso payload is not being changed somewhere before the point at which WordPress observes the request.
At this stage we don’t want to keep changing settings or code blindly.
Question
For current Discourse/DiscourseConnect, what is the best way to determine exactly which payload and secret Discourse is using when it calculates the expected HMAC?
Is there a recommended debugging/logging method that would allow us to compare the Discourse-side HMAC input with the WordPress-side input without exposing the actual secret publicly?
If there are any known issues involving WordPress/PHP handling, URL encoding, Base64 payloads, reverse proxies, or hosted Discourse that could produce this situation, I’d also appreciate pointers.
I can provide sanitized request/callback values, relevant WordPress code, and logs if needed.
Hi Richard. WP-Discourse is installed, but its DiscourseConnect provider and login-sync functions are disabled.
We’re using a small custom WordPress plugin as the DiscourseConnect provider. It receives the sso and sig parameters from Discourse, verifies the incoming HMAC using the shared secret, then builds/signs the response back to Discourse.
The failure is currently occurring on the incoming request from Discourse to WordPress — our fresh diagnostic shows the HMAC recomputation does not match the sig received from Discourse.
Happy to post the relevant PHP callback/validation code. I’ll remove configuration values/secrets before posting it.
Yes — you’re correct. My wording in the original post was backwards.
The current failure is Discourse → WordPress.
Discourse generates the DiscourseConnect request containing sso and sig. WordPress receives it, and our custom EA provider attempts to verify the signature. That incoming HMAC verification fails, so WordPress stops there. It does not get as far as returning the authenticated user payload to Discourse.
WP-Discourse is installed, but its DiscourseConnect provider and user-login sync functions are disabled. We’re currently using the custom EA provider because we want the EA account/profile layer to remain under our control.
I’ll post the relevant callback/config/HMAC-validation code below with all secrets and private configuration removed.
Thanks Richard. I checked that point against PHP/WordPress request handling and the current Discourse signing flow.
In our callback we read from $_GET, so PHP has already URL-decoded the query parameters before EA receives them. wp_unslash() is only reversing WordPress’s request slashing; it is not doing URL decoding.
Applying urldecode() again at that point would double-decode the value and can turn a Base64 + into a space, which would itself break the HMAC.
I’ve also confirmed our fresh diagnostic still fails the HMAC after the WordPress and Discourse secrets were corrected to match.
So I don’t think replacing wp_unslash() with urldecode() is the right fix for this particular callback path. I’m continuing to trace where the signed payload may be diverging before it reaches PHP.