diff options
-rw-r--r-- | templates/user/auth/webauthn.tmpl | 33 | ||||
-rw-r--r-- | templates/user/auth/webauthn_error.tmpl | 22 | ||||
-rw-r--r-- | templates/user/settings/security/webauthn.tmpl | 6 | ||||
-rw-r--r-- | web_src/css/form.css | 4 | ||||
-rw-r--r-- | web_src/js/features/user-auth-webauthn.js | 43 | ||||
-rw-r--r-- | web_src/js/utils.test.js | 15 |
6 files changed, 57 insertions, 66 deletions
diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl index f1c4f29fd9..ef04886405 100644 --- a/templates/user/auth/webauthn.tmpl +++ b/templates/user/auth/webauthn.tmpl @@ -1,21 +1,22 @@ {{template "base/head" .}} -<div class="user signin webauthn-prompt"> - <div class="ui middle centered very relaxed page grid"> +<div role="main" aria-label="{{.Title}}" class="page-content user signin webauthn-prompt"> + <div class="ui page grid"> <div class="column center aligned"> - <h3 class="ui top attached header"> - {{.locale.Tr "twofa"}} - </h3> - {{template "user/auth/webauthn_error" .}} - <div class="ui attached segment"> - {{svg "octicon-key" 56}} - <h3>{{.locale.Tr "webauthn_insert_key"}}</h3> - {{template "base/alert" .}} - <p>{{.locale.Tr "webauthn_sign_in"}}</p> - </div> - <div class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.locale.Tr "webauthn_press_button"}} </div> - <div class="ui attached segment"> - <a href="{{AppSubUrl}}/user/two_factor">{{.locale.Tr "webauthn_use_twofa"}}</a> - </div> + {{template "user/auth/webauthn_error" .}} + <h3 class="ui top attached header">{{.locale.Tr "twofa"}}</h3> + <div class="ui attached segment"> + {{svg "octicon-key" 56}} + <h3>{{.locale.Tr "webauthn_insert_key"}}</h3> + {{template "base/alert" .}} + <p>{{.locale.Tr "webauthn_sign_in"}}</p> + </div> + <div class="ui attached segment"> + <div class="ui active indeterminate inline loader"></div> + {{.locale.Tr "webauthn_press_button"}} + </div> + <div class="ui attached segment"> + <a href="{{AppSubUrl}}/user/two_factor">{{.locale.Tr "webauthn_use_twofa"}}</a> + </div> </div> </div> </div> diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl index f90882ef12..59029bbf91 100644 --- a/templates/user/auth/webauthn_error.tmpl +++ b/templates/user/auth/webauthn_error.tmpl @@ -1,13 +1,13 @@ -<div id="webauthn-error" class="ui small gt-hidden"> - <div class="content ui negative message gt-df gt-fc gt-gap-3"> - <div class="header">{{.locale.Tr "webauthn_error"}}</div> - <div id="webauthn-error-msg"></div> - <div class="gt-hidden" data-webauthn-error-msg="browser">{{.locale.Tr "webauthn_unsupported_browser"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="unknown">{{.locale.Tr "webauthn_error_unknown"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="insecure">{{.locale.Tr "webauthn_error_insecure"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="unable-to-process">{{.locale.Tr "webauthn_error_unable_to_process"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="duplicated">{{.locale.Tr "webauthn_error_duplicated"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="empty">{{.locale.Tr "webauthn_error_empty"}}</div> - <div class="gt-hidden" data-webauthn-error-msg="timeout">{{.locale.Tr "webauthn_error_timeout"}}</div> +<div id="webauthn-error" class="ui negative message gt-hidden"> + <div class="header">{{.locale.Tr "webauthn_error"}}</div> + <div id="webauthn-error-msg" class="gt-pt-3"></div> + <div class="gt-hidden"> + <div data-webauthn-error-msg="browser">{{.locale.Tr "webauthn_unsupported_browser"}}</div> + <div data-webauthn-error-msg="unknown">{{.locale.Tr "webauthn_error_unknown"}}</div> + <div data-webauthn-error-msg="insecure">{{.locale.Tr "webauthn_error_insecure"}}</div> + <div data-webauthn-error-msg="unable-to-process">{{.locale.Tr "webauthn_error_unable_to_process"}}</div> + <div data-webauthn-error-msg="duplicated">{{.locale.Tr "webauthn_error_duplicated"}}</div> + <div data-webauthn-error-msg="empty">{{.locale.Tr "webauthn_error_empty"}}</div> + <div data-webauthn-error-msg="timeout">{{.locale.Tr "webauthn_error_timeout"}}</div> </div> </div> diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl index e541f764bc..ec1df7cdcd 100644 --- a/templates/user/settings/security/webauthn.tmpl +++ b/templates/user/settings/security/webauthn.tmpl @@ -1,6 +1,4 @@ -<h4 class="ui top attached header"> -{{.locale.Tr "settings.webauthn"}} -</h4> +<h4 class="ui top attached header">{{.locale.Tr "settings.webauthn"}}</h4> <div class="ui attached segment"> <p>{{.locale.Tr "settings.webauthn_desc" | Str2html}}</p> {{template "user/auth/webauthn_error" .}} @@ -20,7 +18,6 @@ {{end}} </div> <div class="ui form"> - {{.CsrfTokenHtml}} <div class="required field"> <label for="nickname">{{.locale.Tr "settings.webauthn_nickname"}}</label> <input id="nickname" name="nickname" type="text" required> @@ -29,7 +26,6 @@ </div> </div> - <div class="ui g-modal-confirm delete modal" id="delete-registration"> <div class="header"> {{svg "octicon-trash"}} diff --git a/web_src/css/form.css b/web_src/css/form.css index 9e1a72380b..dfa7208ca2 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -394,10 +394,6 @@ textarea:focus, margin: 0; } -.user.signin.webauthn-prompt { - margin-top: 15px; -} - .repository.new.repo form, .repository.new.migrate form, .repository.new.fork form { diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index 8085313d22..c4c2356cb3 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -1,11 +1,9 @@ import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; -import {showElem, hideElem} from '../utils/dom.js'; +import {showElem} from '../utils/dom.js'; const {appSubUrl, csrfToken} = window.config; export async function initUserAuthWebAuthn() { - hideElem('#webauthn-error'); - const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); if (!elPrompt) { return; @@ -25,10 +23,10 @@ export async function initUserAuthWebAuthn() { for (const cred of options.publicKey.allowCredentials) { cred.id = decodeURLEncodedBase64(cred.id); } - const credential = await navigator.credentials.get({ - publicKey: options.publicKey - }); try { + const credential = await navigator.credentials.get({ + publicKey: options.publicKey + }); await verifyAssertion(credential); } catch (err) { if (!options.publicKey.extensions?.appid) { @@ -36,10 +34,10 @@ export async function initUserAuthWebAuthn() { return; } delete options.publicKey.extensions.appid; - const credential = await navigator.credentials.get({ - publicKey: options.publicKey - }); try { + const credential = await navigator.credentials.get({ + publicKey: options.publicKey + }); await verifyAssertion(credential); } catch (err) { webAuthnError('general', err.message); @@ -48,7 +46,7 @@ export async function initUserAuthWebAuthn() { } async function verifyAssertion(assertedCredential) { - // Move data into Arrays incase it is super long + // Move data into Arrays in case it is super long const authData = new Uint8Array(assertedCredential.response.authenticatorData); const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); const rawId = new Uint8Array(assertedCredential.rawId); @@ -137,15 +135,11 @@ function webAuthnError(errorType, message) { function detectWebAuthnSupport() { if (!window.isSecureContext) { - document.getElementById('register-button').disabled = true; - document.getElementById('login-button').disabled = true; webAuthnError('insecure'); return false; } if (typeof window.PublicKeyCredential !== 'function') { - document.getElementById('register-button').disabled = true; - document.getElementById('login-button').disabled = true; webAuthnError('browser'); return false; } @@ -158,15 +152,13 @@ export function initUserAuthWebAuthnRegister() { if (!elRegister) { return; } - - hideElem('#webauthn-error'); - - elRegister.addEventListener('click', (e) => { + if (!detectWebAuthnSupport()) { + elRegister.disabled = true; + return; + } + elRegister.addEventListener('click', async (e) => { e.preventDefault(); - if (!detectWebAuthnSupport()) { - return; - } - webAuthnRegisterRequest(); + await webAuthnRegisterRequest(); }); } @@ -203,15 +195,12 @@ async function webAuthnRegisterRequest() { } } - let credential; try { - credential = await navigator.credentials.create({ + const credential = await navigator.credentials.create({ publicKey: options.publicKey }); + await webauthnRegistered(credential); } catch (err) { webAuthnError('unknown', err); - return; } - - webauthnRegistered(credential); } diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index cf73b63b99..812ef3bedf 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -133,8 +133,17 @@ test('toAbsoluteUrl', () => { expect(() => toAbsoluteUrl('path')).toThrowError('unsupported'); }); +const uint8array = (s) => new TextEncoder().encode(s); test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { - expect(encodeURLEncodedBase64(decodeURLEncodedBase64('foo'))).toEqual('foo'); // No = padding - expect(encodeURLEncodedBase64(decodeURLEncodedBase64('a-minus'))).toEqual('a-minus'); - expect(encodeURLEncodedBase64(decodeURLEncodedBase64('_underscorc'))).toEqual('_underscorc'); + expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" + expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" + + expect(decodeURLEncodedBase64('QUE/')).toEqual(uint8array('AA?')); + expect(decodeURLEncodedBase64('QUF+')).toEqual(uint8array('AA~')); + expect(decodeURLEncodedBase64('QUE_')).toEqual(uint8array('AA?')); + expect(decodeURLEncodedBase64('QUF-')).toEqual(uint8array('AA~')); + + expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ==" + expect(decodeURLEncodedBase64('YQ')).toEqual(uint8array('a')); + expect(decodeURLEncodedBase64('YQ==')).toEqual(uint8array('a')); }); |