你好,
我正在配置 WordPress 站点与托管 Discourse 实例之间的 DiscourseConnect,但遇到了持续的 HMAC 签名验证失败问题。
配置情况
我们进行了相当广泛的调试,并已将问题范围大幅缩小。
我们已确认的事项
wp_unslash() 不会改变回调值。
- EA/WordPress 的验证输入与从 WordPress 观察到的查询字符串中重构的参数相匹配。
- 预期的回调代码确实在执行。
- 部署的源文件与我们的构建清单一致。
- 我们已检查了错误文件和重复定义的问题。
- 在最新修正后,PHP-FPM 已重启,因此这不是旧的 PHP 进程/opcache 状态问题。
- 在这些更改之后,一个全新的 DiscourseConnect 请求仍然导致 HMAC 验证失败。
换句话说,当前的失败可以在新请求中复现。
我们尚未能证明的事项
- Discourse 在运行时实际使用的密钥与 WordPress 使用的密钥在字节级别上完全一致。
sso 负载在 WordPress 观察到请求之前的某个环节没有被修改。
在此阶段,我们不想盲目地继续更改设置或代码。
问题
对于当前的 Discourse/DiscourseConnect,确定 Discourse 在计算预期 HMAC 时确切使用哪个负载和密钥的最佳方法是什么?
是否有推荐的调试/日志记录方法,可以让我们在不公开实际密钥的情况下,比较 Discourse 端的 HMAC 输入与 WordPress 端的输入?
如果存在任何已知的涉及 WordPress/PHP 处理、URL 编码、Base64 负载、反向代理或托管 Discourse 的问题,可能会导致这种情况,我也很乐意得到一些指引。
如有需要,我可以提供经过脱敏处理的请求/回调值、相关的 WordPress 代码和日志。
谢谢。
RGJ
(Richard - Communiteq)
2
那你在 WordPress 这边用的是什么?是 WP-Discourse 插件吗?如果不是,能分享一下你的代码吗?
你好,Richard。WP-Discourse 已经安装,但其 DiscourseConnect 提供程序(provider)和登录同步(login-sync)功能目前处于禁用状态。
我们使用了一个小型的自定义 WordPress 插件作为 DiscourseConnect 提供程序。它会接收来自 Discourse 的 sso 和 sig 参数,使用共享密钥验证传入的 HMAC,然后构建并签署响应发回给 Discourse。
目前故障发生在从 Discourse 到 WordPress 的传入请求上——我们最新的诊断结果显示,重新计算的 HMAC 与从 Discourse 接收到的 sig 不匹配。
我很乐意分享相关的 PHP 回调/验证代码。在发布之前,我会移除其中的配置值和密钥。
RGJ
(Richard - Communiteq)
4
所以是反过来的,对吗?
显而易见的方法是直接使用 WP-Discourse 插件,但你很可能有充分的理由不使用它。
是的,请发布代码。
是的——你说得对。我在原帖中的表述颠倒了。
当前的故障发生在 Discourse → WordPress 方向。
Discourse 会生成包含 sso 和 sig 的 DiscourseConnect 请求。WordPress 接收该请求后,我们的自定义 EA 提供商尝试验证签名。入站的 HMAC 验证失败,因此 WordPress 在此处停止。它尚未进行到将已认证的用户负载返回给 Discourse 的步骤。
WP-Discourse 已安装,但其 DiscourseConnect 提供商和用户登录同步功能已被禁用。我们目前使用自定义 EA 提供商,是因为我们希望 EA 账户/个人资料层保持在我们自己的控制之下。
我将在下方发布相关的回调/配置/HMAC 验证代码,其中已移除所有密钥和私有配置。
以下是 EA 自定义 WordPress 提供程序中,经过净化的常规传入 DiscourseConnect 验证路径(Discourse → WordPress)。
<?php
// 配置常量在其他地方定义;此处省略其私有值。
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 社区登录',
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() {
// 响应头设置已省略。
if ( ! self::enabled() ) {
$this->fail( '社区登录已禁用。', 503 );
}
if ( ! self::ready() ) {
$this->fail( '社区登录未配置。', 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(
'无效的社区登录请求。请从社区重新开始。'
);
}
// 后续的 WordPress 登录和经过身份验证的响应代码已省略。
}
}
// 由插件引导程序实例化:
new EA_Discourse_Connect();
RGJ
(Richard - Communiteq)
7
你应该使用 urldecode() 来解码参数,而不是 wp_unslash()