错误信息
Unexpected registration response origin "http://localhost:3000", expected "https://app.example.com"
也可能输出为
- Unexpected authentication response origin "http://localhost:3000", expected "https://app.example.com"
- Unexpected registration response challenge "b3RoZXI", expected "c2VydmVyLWNoYWxsZW5nZS0xMjM"
第一个带引号的值是浏览器报告的源,第二个是你的配置。登录时,文本中写的是“authentication response”;挑战值不匹配时,同一个库会报出“Unexpected registration response challenge …”。
SimpleWebAuthn (@simplewebauthn/server) — verifyRegistrationResponse / verifyAuthenticationResponse
含义
浏览器在 `clientDataJSON`(认证器签名的数据)中记录了页面的源,它与你的服务器传给 SimpleWebAuthn 的 `expectedOrigin` 不一致。这项检查是为了防止在一个站点上执行的凭证流程(例如在仿冒的钓鱼网站上)被另一个站点接受。认证器本身没有问题;问题在于服务器认为自己的页面所在的位置,与流程实际运行的位置不一致。
原因
在开发环境中配置了生产环境的源(或者反过来)
常见`expectedOrigin` 被硬编码为生产环境的 URL,而测试时用的却是 `http://localhost:3000`、预览部署或隧道(ngrok、Cloudflare Tunnel)的 URL。
登录页面由多个源提供
常见`www.` 和根域名、多个国家/地区域名,或者单独的 `accounts.` 子域名。每个可以运行该流程的源都必须列出来。
原生 Android 应用
偶见Credential Manager 报告的源是 `android:apk-key-hash:…`,而不是你的 Web 源。`expectedOrigin` 中必须包含该应用签名密钥的哈希。
修复方法
- 1.
显式列出每一个合法的源
`expectedOrigin` 接受数组。只列出你控制的源;切勿直接采用请求本身携带的源。
js const verification = await verifyRegistrationResponse({ response: body, expectedChallenge: session.currentChallenge, // Every origin that legitimately hosts your sign-up page, exactly as browsers report it: expectedOrigin: ["https://app.example.com", "https://www.example.com"], expectedRPID: "example.com", }); - 2.
添加 Android 应用的源
计算该应用签名证书的 SHA-256,以 base64url 编码,然后按 `android:apk-key-hash:` 的格式添加进去。
js // Android apps report "android:apk-key-hash:<base64url SHA-256 of the app signing certificate>". // SHA-256 fingerprint from Play Console or: keytool -list -v -keystore release.jks | grep SHA256 const fingerprintHex = process.env.ANDROID_SIGNING_CERT_SHA256.replaceAll(":", ""); const androidOrigin = "android:apk-key-hash:" + Buffer.from(fingerprintHex, "hex").toString("base64url"); const verification = await verifyRegistrationResponse({ response: body, expectedChallenge: session.currentChallenge, expectedOrigin: ["https://example.com", androidOrigin], expectedRPID: "example.com", });