From df2adb203c423dbcbd8672705027dfd1eb868053 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 29 Jan 2023 10:01:19 +0000 Subject: [PATCH 01/18] Fix Uncaught DOMException: Failed to execute 'atob' on 'Window' There is a missing import within the `uint8-to-base64` javascript package which assumes that `atob` and `btoa` are present and exported instead of using the `window.atob` and `window.btoa` functions. This previously worked but as far as I can see things have become more strict and this no longer works. The dependency is small and I do not believe that we gain much from having this code as an external dependency. I think instead we should just consume this dependency and bring the code directly into Gitea itself - the code is itself just some standard incantation for creating base64 arrays in javascript. Therefore this PR simply removes the dependency on `uint8-to-base64` and rewrites the functions used in it. Fix #22507 Signed-off-by: Andrew Thornton --- package-lock.json | 11 ---------- package.json | 1 - web_src/js/features/user-auth-webauthn.js | 25 ++++++++++++++++------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60f0a0f8e623..50dddde13f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "swagger-ui-dist": "4.15.5", "tippy.js": "6.3.7", "tributejs": "5.1.3", - "uint8-to-base64": "0.2.0", "vue": "3.2.45", "vue-bar-graph": "2.0.0", "vue-loader": "17.0.1", @@ -8856,11 +8855,6 @@ "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", "dev": true }, - "node_modules/uint8-to-base64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz", - "integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA==" - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -16366,11 +16360,6 @@ "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", "dev": true }, - "uint8-to-base64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz", - "integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA==" - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 842804211cf8..7821fbb58005 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "swagger-ui-dist": "4.15.5", "tippy.js": "6.3.7", "tributejs": "5.1.3", - "uint8-to-base64": "0.2.0", "vue": "3.2.45", "vue-bar-graph": "2.0.0", "vue-loader": "17.0.1", diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index f11a49864ded..c86f6314edf5 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -1,8 +1,19 @@ import $ from 'jquery'; -import {encode, decode} from 'uint8-to-base64'; const {appSubUrl, csrfToken} = window.config; +function encodeToBase64(toEncode) { + const output = []; + for (let i = 0; i < toEncode.length; i++) { + output.push(String.fromCharCode(toEncode[i])); + } + return window.btoa(output.join('')); +} + +function decodeFromBase64(toDecode) { + return Uint8Array.from(window.atob(toDecode), (c) => c.charCodeAt(0)); +} + export function initUserAuthWebAuthn() { if ($('.user.signin.webauthn-prompt').length === 0) { return; @@ -14,9 +25,9 @@ export function initUserAuthWebAuthn() { $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) .done((makeAssertionOptions) => { - makeAssertionOptions.publicKey.challenge = decode(makeAssertionOptions.publicKey.challenge); + makeAssertionOptions.publicKey.challenge = decodeFromBase64(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 = decodeFromBase64(makeAssertionOptions.publicKey.allowCredentials[i].id); } navigator.credentials.get({ publicKey: makeAssertionOptions.publicKey @@ -87,7 +98,7 @@ function verifyAssertion(assertedCredential) { // Encode an ArrayBuffer into a base64 string. function bufferEncode(value) { - return encode(value) + return encodeToBase64(value) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); @@ -184,11 +195,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 = decodeFromBase64(makeCredentialOptions.publicKey.challenge); + makeCredentialOptions.publicKey.user.id = decodeFromBase64(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 = decodeFromBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id); } } From f40e5161a22fbc6d2822430ad859295eb7e39c5e Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 29 Jan 2023 12:31:08 +0000 Subject: [PATCH 02/18] Update web_src/js/features/user-auth-webauthn.js Co-authored-by: delvh --- web_src/js/features/user-auth-webauthn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index c86f6314edf5..180309eefb9f 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -2,7 +2,7 @@ import $ from 'jquery'; const {appSubUrl, csrfToken} = window.config; -function encodeToBase64(toEncode) { +function encodeToBase64(toEncode /* Uint8Array */) { const output = []; for (let i = 0; i < toEncode.length; i++) { output.push(String.fromCharCode(toEncode[i])); From dcd63076afe9ae81b1ce46ed6b73c89a369e677b Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 29 Jan 2023 21:21:14 +0000 Subject: [PATCH 03/18] Include findings from #22654 Signed-off-by: Andrew Thornton --- web_src/js/features/user-auth-webauthn.js | 37 ++++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index c86f6314edf5..3b00bf1e9644 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -25,9 +25,9 @@ export function initUserAuthWebAuthn() { $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) .done((makeAssertionOptions) => { - makeAssertionOptions.publicKey.challenge = decodeFromBase64(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 = decodeFromBase64(makeAssertionOptions.publicKey.allowCredentials[i].id); + makeAssertionOptions.publicKey.allowCredentials[i].id = decodeURLEncodedBase64(makeAssertionOptions.publicKey.allowCredentials[i].id); } navigator.credentials.get({ publicKey: makeAssertionOptions.publicKey @@ -67,14 +67,14 @@ function verifyAssertion(assertedCredential) { type: 'POST', data: JSON.stringify({ id: assertedCredential.id, - rawId: bufferEncode(rawId), + rawId: bufferURLEncodedBase64(rawId), type: assertedCredential.type, clientExtensionResults: assertedCredential.getClientExtensionResults(), response: { - authenticatorData: bufferEncode(authData), - clientDataJSON: bufferEncode(clientDataJSON), - signature: bufferEncode(sig), - userHandle: bufferEncode(userHandle), + authenticatorData: bufferURLEncodedBase64(authData), + clientDataJSON: bufferURLEncodedBase64(clientDataJSON), + signature: bufferURLEncodedBase64(sig), + userHandle: bufferURLEncodedBase64(userHandle), }, }), contentType: 'application/json; charset=utf-8', @@ -96,14 +96,21 @@ function verifyAssertion(assertedCredential) { }); } -// Encode an ArrayBuffer into a base64 string. -function bufferEncode(value) { +// Encode an ArrayBuffer into a URLEncoded base64 string. +function bufferURLEncodedBase64(value) { return encodeToBase64(value) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } +// Dccode a URLEncoded base64 to an ArrayBuffer string. +function decodeURLEncodedBase64(value) { + return decodeFromBase64(value + .replace(/_/g, '/') + .replace(/-/g, '+')); +} + function webauthnRegistered(newCredential) { const attestationObject = new Uint8Array(newCredential.response.attestationObject); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); @@ -115,11 +122,11 @@ function webauthnRegistered(newCredential) { headers: {'X-Csrf-Token': csrfToken}, data: JSON.stringify({ id: newCredential.id, - rawId: bufferEncode(rawId), + rawId: bufferURLEncodedBase64(rawId), type: newCredential.type, response: { - attestationObject: bufferEncode(attestationObject), - clientDataJSON: bufferEncode(clientDataJSON), + attestationObject: bufferURLEncodedBase64(attestationObject), + clientDataJSON: bufferURLEncodedBase64(clientDataJSON), }, }), dataType: 'json', @@ -195,11 +202,11 @@ function webAuthnRegisterRequest() { }).done((makeCredentialOptions) => { $('#nickname').closest('div.field').removeClass('error'); - makeCredentialOptions.publicKey.challenge = decodeFromBase64(makeCredentialOptions.publicKey.challenge); - makeCredentialOptions.publicKey.user.id = decodeFromBase64(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 = decodeFromBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id); + makeCredentialOptions.publicKey.excludeCredentials[i].id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id); } } From f03824463e3993d1ab7fa845cf369daaf98e1516 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 30 Jan 2023 21:08:27 +0000 Subject: [PATCH 04/18] As per wxiaoguang Signed-off-by: Andrew Thornton --- modules/auth/webauthn/webauthn.go | 2 +- package-lock.json | 11 +++++++ package.json | 1 + web_src/js/features/user-auth-webauthn.js | 35 ++++++++--------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/modules/auth/webauthn/webauthn.go b/modules/auth/webauthn/webauthn.go index d08f7bf7cc71..937da872ca1a 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/package-lock.json b/package-lock.json index 50dddde13f89..60f0a0f8e623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "swagger-ui-dist": "4.15.5", "tippy.js": "6.3.7", "tributejs": "5.1.3", + "uint8-to-base64": "0.2.0", "vue": "3.2.45", "vue-bar-graph": "2.0.0", "vue-loader": "17.0.1", @@ -8855,6 +8856,11 @@ "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", "dev": true }, + "node_modules/uint8-to-base64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz", + "integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -16360,6 +16366,11 @@ "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", "dev": true }, + "uint8-to-base64": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz", + "integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 7821fbb58005..842804211cf8 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "swagger-ui-dist": "4.15.5", "tippy.js": "6.3.7", "tributejs": "5.1.3", + "uint8-to-base64": "0.2.0", "vue": "3.2.45", "vue-bar-graph": "2.0.0", "vue-loader": "17.0.1", diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index e7116210768a..9c9fffd99527 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -1,19 +1,8 @@ import $ from 'jquery'; +import {encode, decode} from 'uint8-to-base64'; const {appSubUrl, csrfToken} = window.config; -function encodeToBase64(toEncode /* Uint8Array */) { - const output = []; - for (let i = 0; i < toEncode.length; i++) { - output.push(String.fromCharCode(toEncode[i])); - } - return window.btoa(output.join('')); -} - -function decodeFromBase64(toDecode) { - return Uint8Array.from(window.atob(toDecode), (c) => c.charCodeAt(0)); -} - export function initUserAuthWebAuthn() { if ($('.user.signin.webauthn-prompt').length === 0) { return; @@ -67,14 +56,14 @@ function verifyAssertion(assertedCredential) { type: 'POST', data: JSON.stringify({ id: assertedCredential.id, - rawId: bufferURLEncodedBase64(rawId), + rawId: encodeURLEncodedBase64(rawId), type: assertedCredential.type, clientExtensionResults: assertedCredential.getClientExtensionResults(), response: { - authenticatorData: bufferURLEncodedBase64(authData), - clientDataJSON: bufferURLEncodedBase64(clientDataJSON), - signature: bufferURLEncodedBase64(sig), - userHandle: bufferURLEncodedBase64(userHandle), + authenticatorData: encodeURLEncodedBase64(authData), + clientDataJSON: encodeURLEncodedBase64(clientDataJSON), + signature: encodeURLEncodedBase64(sig), + userHandle: encodeURLEncodedBase64(userHandle), }, }), contentType: 'application/json; charset=utf-8', @@ -97,8 +86,8 @@ function verifyAssertion(assertedCredential) { } // Encode an ArrayBuffer into a URLEncoded base64 string. -function bufferURLEncodedBase64(value) { - return encodeToBase64(value) +function encodeURLEncodedBase64(value) { + return encode(value) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); @@ -106,7 +95,7 @@ function bufferURLEncodedBase64(value) { // Dccode a URLEncoded base64 to an ArrayBuffer string. function decodeURLEncodedBase64(value) { - return decodeFromBase64(value + return decode(value .replace(/_/g, '/') .replace(/-/g, '+')); } @@ -122,11 +111,11 @@ function webauthnRegistered(newCredential) { headers: {'X-Csrf-Token': csrfToken}, data: JSON.stringify({ id: newCredential.id, - rawId: bufferURLEncodedBase64(rawId), + rawId: encodeURLEncodedBase64(rawId), type: newCredential.type, response: { - attestationObject: bufferURLEncodedBase64(attestationObject), - clientDataJSON: bufferURLEncodedBase64(clientDataJSON), + attestationObject: encodeURLEncodedBase64(attestationObject), + clientDataJSON: encodeURLEncodedBase64(clientDataJSON), }, }), dataType: 'json', From b37e6a1aa16c57bab6852f8f0495721b34551b3e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 31 Jan 2023 20:17:49 +0000 Subject: [PATCH 05/18] fix test Signed-off-by: Andrew Thornton --- modules/auth/webauthn/webauthn_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auth/webauthn/webauthn_test.go b/modules/auth/webauthn/webauthn_test.go index 1beeb64cd6ca..15a8d7182833 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) } From 104a864965e14c44406d4d7d2bfa8177b5b6262e Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 31 Jan 2023 20:15:37 +0000 Subject: [PATCH 06/18] Clean up WebAuthn javascript code and remove JQuery code There were several issues with the WebAuthn registration and testing code and the style was very old javascript with jquery callbacks. This PR uses async and fetch to replace the JQuery code. Ref #22651 Signed-off-by: Andrew Thornton --- routers/web/user/setting/security/webauthn.go | 6 +- templates/user/auth/webauthn.tmpl | 2 +- templates/user/auth/webauthn_error.tmpl | 30 +- .../user/settings/security/webauthn.tmpl | 2 +- web_src/js/features/user-auth-webauthn.js | 289 ++++++++++-------- 5 files changed, 178 insertions(+), 151 deletions(-) diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go index 005431886746..826562f15748 100644 --- a/routers/web/user/setting/security/webauthn.go +++ b/routers/web/user/setting/security/webauthn.go @@ -6,6 +6,8 @@ package security import ( "errors" "net/http" + "strconv" + "time" "code.gitea.io/gitea/models/auth" wa "code.gitea.io/gitea/modules/auth/webauthn" @@ -23,8 +25,8 @@ import ( func WebAuthnRegister(ctx *context.Context) { form := web.GetForm(ctx).(*forms.WebauthnRegistrationForm) if form.Name == "" { - ctx.Error(http.StatusConflict) - return + // Set name to the hexadecimal of the current time + form.Name = strconv.FormatInt(time.Now().UnixNano(), 16) } cred, err := auth.GetWebAuthnCredentialByName(ctx.Doer.ID, form.Name) diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl index 304d199da21a..e580b4d13ab0 100644 --- a/templates/user/auth/webauthn.tmpl +++ b/templates/user/auth/webauthn.tmpl @@ -5,6 +5,7 @@

{{.locale.Tr "twofa"}}

+ {{template "user/auth/webauthn_error" .}}

{{.locale.Tr "webauthn_insert_key"}}

@@ -18,5 +19,4 @@
-{{template "user/auth/webauthn_error" .}} {{template "base/footer" .}} diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl index c3cae710f1ef..d309a1a036ae 100644 --- a/templates/user/auth/webauthn_error.tmpl +++ b/templates/user/auth/webauthn_error.tmpl @@ -1,22 +1,16 @@ -