From 19d5b2f922c2defde579a935fbedb680eb8fff18 Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 1 Feb 2023 07:24:10 +0000 Subject: [PATCH] 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 Co-authored-by: wxiaoguang --- modules/auth/webauthn/webauthn.go | 2 +- modules/auth/webauthn/webauthn_test.go | 4 +-- web_src/js/features/user-auth-webauthn.js | 37 ++++++++++++++--------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index d08f7bf7cc7..937da872ca1 100644 --- a/modules/auth/webauthn/webauthn.go +++ b/modules/auth/webauthn/webauthn.go @@ -28,7 +28,7 @@ func Init() { Config: &webauthn.Config{ RPDisplayName: setting.AppName, RPID: setting.Domain, - RPOrigin: appURL, + RPOrigins: []string{appURL}, AuthenticatorSelection: protocol.AuthenticatorSelection{ UserVerification: "discouraged", }, diff --git a/modules/auth/webauthn/webauthn_test.go b/modules/auth/webauthn/webauthn_test.go index 1beeb64cd6c..15a8d718283 100644 --- a/modules/auth/webauthn/webauthn_test.go +++ b/modules/auth/webauthn/webauthn_test.go @@ -15,11 +15,11 @@ func TestInit(t *testing.T) { setting.Domain = "domain" setting.AppName = "AppName" setting.AppURL = "https://domain/" - rpOrigin := "https://domain" + rpOrigin := []string{"https://domain"} Init() assert.Equal(t, setting.Domain, WebAuthn.Config.RPID) assert.Equal(t, setting.AppName, WebAuthn.Config.RPDisplayName) - assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigin) + assert.Equal(t, rpOrigin, WebAuthn.Config.RPOrigins) } diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index f11a49864de..9c9fffd9952 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -14,9 +14,9 @@ export function initUserAuthWebAuthn() { $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) .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++) { - 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({ publicKey: makeAssertionOptions.publicKey @@ -56,14 +56,14 @@ function verifyAssertion(assertedCredential) { type: 'POST', data: JSON.stringify({ id: assertedCredential.id, - rawId: bufferEncode(rawId), + rawId: encodeURLEncodedBase64(rawId), type: assertedCredential.type, clientExtensionResults: assertedCredential.getClientExtensionResults(), response: { - authenticatorData: bufferEncode(authData), - clientDataJSON: bufferEncode(clientDataJSON), - signature: bufferEncode(sig), - userHandle: bufferEncode(userHandle), + authenticatorData: encodeURLEncodedBase64(authData), + clientDataJSON: encodeURLEncodedBase64(clientDataJSON), + signature: encodeURLEncodedBase64(sig), + userHandle: encodeURLEncodedBase64(userHandle), }, }), contentType: 'application/json; charset=utf-8', @@ -85,14 +85,21 @@ function verifyAssertion(assertedCredential) { }); } -// Encode an ArrayBuffer into a base64 string. -function bufferEncode(value) { +// Encode an ArrayBuffer into a URLEncoded base64 string. +function encodeURLEncodedBase64(value) { return encode(value) .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) { const attestationObject = new Uint8Array(newCredential.response.attestationObject); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); @@ -104,11 +111,11 @@ function webauthnRegistered(newCredential) { headers: {'X-Csrf-Token': csrfToken}, data: JSON.stringify({ id: newCredential.id, - rawId: bufferEncode(rawId), + rawId: encodeURLEncodedBase64(rawId), type: newCredential.type, response: { - attestationObject: bufferEncode(attestationObject), - clientDataJSON: bufferEncode(clientDataJSON), + attestationObject: encodeURLEncodedBase64(attestationObject), + clientDataJSON: encodeURLEncodedBase64(clientDataJSON), }, }), dataType: 'json', @@ -184,11 +191,11 @@ function webAuthnRegisterRequest() { }).done((makeCredentialOptions) => { $('#nickname').closest('div.field').removeClass('error'); - makeCredentialOptions.publicKey.challenge = decode(makeCredentialOptions.publicKey.challenge); - makeCredentialOptions.publicKey.user.id = decode(makeCredentialOptions.publicKey.user.id); + makeCredentialOptions.publicKey.challenge = decodeURLEncodedBase64(makeCredentialOptions.publicKey.challenge); + makeCredentialOptions.publicKey.user.id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.user.id); if (makeCredentialOptions.publicKey.excludeCredentials) { 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); } }