Fix bugs with WebAuthn preventing sign in and registration. (#22651)

This PR fixes two bugs with Webauthn support:

* There was a longstanding bug within webauthn due to the backend using
URLEncodedBase64 but the javascript using decoding using plain base64.
This causes intermittent issues with users reporting decoding errors.
* Following the recent upgrade to webauthn there was a change in the way
the library expects RPOrigins to be configured. This leads to the
Relying Party Origin not being configured and prevents registration.

Fix #22507

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
zeripath 2023-02-01 07:24:10 +00:00 committed by GitHub
parent 2871ea0809
commit 19d5b2f922
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 25 additions and 18 deletions

View file

@ -28,7 +28,7 @@ func Init() {
Config: &webauthn.Config{ Config: &webauthn.Config{
RPDisplayName: setting.AppName, RPDisplayName: setting.AppName,
RPID: setting.Domain, RPID: setting.Domain,
RPOrigin: appURL, RPOrigins: []string{appURL},
AuthenticatorSelection: protocol.AuthenticatorSelection{ AuthenticatorSelection: protocol.AuthenticatorSelection{
UserVerification: "discouraged", UserVerification: "discouraged",
}, },

View file

@ -15,11 +15,11 @@ func TestInit(t *testing.T) {
setting.Domain = "domain" setting.Domain = "domain"
setting.AppName = "AppName" setting.AppName = "AppName"
setting.AppURL = "https://domain/" setting.AppURL = "https://domain/"
rpOrigin := "https://domain" rpOrigin := []string{"https://domain"}
Init() Init()
assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) assert.Equal(t, setting.Domain, WebAuthn.Config.RPID)
assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName)
assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigin) assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins)
} }

View file

@ -14,9 +14,9 @@ export function initUserAuthWebAuthn() {
$.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {})
.done((makeAssertionOptions) => { .done((makeAssertionOptions) => {
makeAssertionOptions.publicKey.challenge = decode(makeAssertionOptions.publicKey.challenge); makeAssertionOptions.publicKey.challenge = decodeURLEncodedBase64(makeAssertionOptions.publicKey.challenge);
for (let i = 0; i < makeAssertionOptions.publicKey.allowCredentials.length; i++) { for (let i = 0; i < makeAssertionOptions.publicKey.allowCredentials.length; i++) {
makeAssertionOptions.publicKey.allowCredentials[i].id = decode(makeAssertionOptions.publicKey.allowCredentials[i].id); makeAssertionOptions.publicKey.allowCredentials[i].id = decodeURLEncodedBase64(makeAssertionOptions.publicKey.allowCredentials[i].id);
} }
navigator.credentials.get({ navigator.credentials.get({
publicKey: makeAssertionOptions.publicKey publicKey: makeAssertionOptions.publicKey
@ -56,14 +56,14 @@ function verifyAssertion(assertedCredential) {
type: 'POST', type: 'POST',
data: JSON.stringify({ data: JSON.stringify({
id: assertedCredential.id, id: assertedCredential.id,
rawId: bufferEncode(rawId), rawId: encodeURLEncodedBase64(rawId),
type: assertedCredential.type, type: assertedCredential.type,
clientExtensionResults: assertedCredential.getClientExtensionResults(), clientExtensionResults: assertedCredential.getClientExtensionResults(),
response: { response: {
authenticatorData: bufferEncode(authData), authenticatorData: encodeURLEncodedBase64(authData),
clientDataJSON: bufferEncode(clientDataJSON), clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
signature: bufferEncode(sig), signature: encodeURLEncodedBase64(sig),
userHandle: bufferEncode(userHandle), userHandle: encodeURLEncodedBase64(userHandle),
}, },
}), }),
contentType: 'application/json; charset=utf-8', contentType: 'application/json; charset=utf-8',
@ -85,14 +85,21 @@ function verifyAssertion(assertedCredential) {
}); });
} }
// Encode an ArrayBuffer into a base64 string. // Encode an ArrayBuffer into a URLEncoded base64 string.
function bufferEncode(value) { function encodeURLEncodedBase64(value) {
return encode(value) return encode(value)
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, ''); .replace(/=/g, '');
} }
// Dccode a URLEncoded base64 to an ArrayBuffer string.
function decodeURLEncodedBase64(value) {
return decode(value
.replace(/_/g, '/')
.replace(/-/g, '+'));
}
function webauthnRegistered(newCredential) { function webauthnRegistered(newCredential) {
const attestationObject = new Uint8Array(newCredential.response.attestationObject); const attestationObject = new Uint8Array(newCredential.response.attestationObject);
const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
@ -104,11 +111,11 @@ function webauthnRegistered(newCredential) {
headers: {'X-Csrf-Token': csrfToken}, headers: {'X-Csrf-Token': csrfToken},
data: JSON.stringify({ data: JSON.stringify({
id: newCredential.id, id: newCredential.id,
rawId: bufferEncode(rawId), rawId: encodeURLEncodedBase64(rawId),
type: newCredential.type, type: newCredential.type,
response: { response: {
attestationObject: bufferEncode(attestationObject), attestationObject: encodeURLEncodedBase64(attestationObject),
clientDataJSON: bufferEncode(clientDataJSON), clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
}, },
}), }),
dataType: 'json', dataType: 'json',
@ -184,11 +191,11 @@ function webAuthnRegisterRequest() {
}).done((makeCredentialOptions) => { }).done((makeCredentialOptions) => {
$('#nickname').closest('div.field').removeClass('error'); $('#nickname').closest('div.field').removeClass('error');
makeCredentialOptions.publicKey.challenge = decode(makeCredentialOptions.publicKey.challenge); makeCredentialOptions.publicKey.challenge = decodeURLEncodedBase64(makeCredentialOptions.publicKey.challenge);
makeCredentialOptions.publicKey.user.id = decode(makeCredentialOptions.publicKey.user.id); makeCredentialOptions.publicKey.user.id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.user.id);
if (makeCredentialOptions.publicKey.excludeCredentials) { if (makeCredentialOptions.publicKey.excludeCredentials) {
for (let i = 0; i < makeCredentialOptions.publicKey.excludeCredentials.length; i++) { for (let i = 0; i < makeCredentialOptions.publicKey.excludeCredentials.length; i++) {
makeCredentialOptions.publicKey.excludeCredentials[i].id = decode(makeCredentialOptions.publicKey.excludeCredentials[i].id); makeCredentialOptions.publicKey.excludeCredentials[i].id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id);
} }
} }