diff --git a/app/assets/javascripts/registerSecurityKey.js b/app/assets/javascripts/registerSecurityKey.js index 993e325e7..69c2c0383 100644 --- a/app/assets/javascripts/registerSecurityKey.js +++ b/app/assets/javascripts/registerSecurityKey.js @@ -7,7 +7,10 @@ .on('click', function(event) { event.preventDefault(); - fetchWebAuthnCreateOptions() + fetch('/webauthn/register') + .then((response) => { + return response.arrayBuffer(); + }) .then((data) => { var options = window.CBOR.decode(data); // triggers browser dialogue to select authenticator @@ -22,45 +25,21 @@ window.location.reload(); }) .catch((error) => { - // there may be other kinds of error we should catch here - // https://github.com/w3c/webauthn/issues/876 - if (error instanceof DOMException) { - console.error(error); - // not all browsers show an error dialogue, so to be safe - // we manually pop one open here (to be improved in future!) - alert('Error communicating with device.\n\n' + error.message); - } else { - // for web requests we need to manually alert the user - // $.ajax seems to log by itself, but that's not visible - alert('Error during registration. Please try again.'); - } + console.error(error); + // some browsers will show an error dialogue for some + // errors; to be safe we always pop up an alert + var message = error.message || error; + alert('Error during registration.\n\n' + message); }); }); }; }; - function fetchWebAuthnCreateOptions() { - var xhrOverride = new XMLHttpRequest(); - xhrOverride.responseType = 'arraybuffer'; - - return $.ajax({ - url: '/webauthn/register', - xhr: () => xhrOverride, - dataType: 'x-binary', - converters: { '* x-binary': (value) => value } - }); - } - function postWebAuthnCreateResponse(response, csrf_token) { - return $.ajax({ - url: '/webauthn/register', + return fetch('/webauthn/register', { method: 'POST', - headers: { - 'X-CSRFToken': csrf_token - }, - processData: false, - contentType: 'application/cbor', - data: window.CBOR.encode({ + headers: { 'X-CSRFToken': csrf_token }, + body: window.CBOR.encode({ attestationObject: new Uint8Array(response.attestationObject), clientDataJSON: new Uint8Array(response.clientDataJSON), }) diff --git a/tests/javascripts/registerSecurityKey.test.js b/tests/javascripts/registerSecurityKey.test.js index 3d265fe6f..03b675e1d 100644 --- a/tests/javascripts/registerSecurityKey.test.js +++ b/tests/javascripts/registerSecurityKey.test.js @@ -5,10 +5,16 @@ beforeAll(() => { // disable console.error() so we don't see it in test output // you might need to comment this out to debug some failures jest.spyOn(console, 'error').mockImplementation(() => {}) + + // populate missing values to allow consistent jest.spyOn() + window.fetch = () => {} }) afterAll(() => { require('./support/teardown.js') + + // restore window attributes to their original undefined state + delete window.fetch }) describe('Register security key', () => { @@ -55,16 +61,19 @@ describe('Register security key', () => { writable: true, }) - jest.spyOn(window.$, 'ajax').mockImplementation((options) => { + jest.spyOn(window, 'fetch').mockImplementation((_url, options = {}) => { // initial fetch of options from the server if (!options.method) { // options from the server are CBOR-encoded webauthnOptions = window.CBOR.encode('options') - return Promise.resolve(webauthnOptions) + + return Promise.resolve({ + ok: true, arrayBuffer: () => webauthnOptions + }) // subsequent POST of credential data to server } else { - decodedData = window.CBOR.decode(options.data) + decodedData = window.CBOR.decode(options.body) expect(decodedData.clientDataJSON).toEqual(new Uint8Array([4,5,6])) expect(decodedData.attestationObject).toEqual(new Uint8Array([1,2,3])) expect(options.headers['X-CSRFToken']).toBe() @@ -76,13 +85,13 @@ describe('Register security key', () => { }) test('alerts if fetching WebAuthn options fails', (done) => { - jest.spyOn(window.$, 'ajax').mockImplementation((options) => { + jest.spyOn(window, 'fetch').mockImplementation((_url, options = {}) => { return Promise.reject('error') }) jest.spyOn(window, 'alert').mockImplementation((msg) => { + expect(msg).toEqual('Error during registration.\n\nerror') done() - expect(msg).toEqual('Error during registration. Please try again.') }) button.click() @@ -100,11 +109,14 @@ describe('Register security key', () => { writable: true, }) - jest.spyOn(window.$, 'ajax').mockImplementation((options) => { + jest.spyOn(window, 'fetch').mockImplementation((_url, options = {}) => { // initial fetch of options from the server if (!options.method) { webauthnOptions = window.CBOR.encode('options') - return Promise.resolve(webauthnOptions) + + return Promise.resolve({ + ok: true, arrayBuffer: () => webauthnOptions + }) // subsequent POST of credential data to server } else { @@ -113,8 +125,8 @@ describe('Register security key', () => { }) jest.spyOn(window, 'alert').mockImplementation((msg) => { + expect(msg).toEqual('Error during registration.\n\nerror') done() - expect(msg).toEqual('Error during registration. Please try again.') }) button.click() @@ -131,15 +143,18 @@ describe('Register security key', () => { writable: true, }) - jest.spyOn(window.$, 'ajax').mockImplementation((options) => { + jest.spyOn(window, 'fetch').mockImplementation((_url, options) => { // initial fetch of options from the server webauthnOptions = window.CBOR.encode('options') - return Promise.resolve(webauthnOptions) + + return Promise.resolve({ + ok: true, arrayBuffer: () => webauthnOptions + }) }) jest.spyOn(window, 'alert').mockImplementation((msg) => { + expect(msg).toEqual('Error during registration.\n\nerror') done() - expect(msg).toEqual('Error communicating with device.\n\nerror') }) button.click()