Files
notifications-admin/tests/javascripts/authenticateSecurityKey.test.js
Leo Hemsted bb7343d846 pass nextUrl through yubikey flow
the next url comes from sign in via a query param, and needs to go to
the POST /webauthn/authenticate endpoint. That endpoint logs the user
in and returns the redirect to the browser, and will take the next from
the request query params to get there.

also moving the window mocks to beforeEach/afterEach ensures that
promise callbacks from previous tests aren't still associated in future
tests to ensure good test isolation.

unfortunately i couldn't get mocking location for a single js test to
work, but by changing the global config i was able to add some query
params that i can expect to be passed through. Don't love this at all
but not quite sure of a good way round this. I think we're not
practicing very good hygiene and best practices with our mocking and
it's really confounding me here.
2021-06-04 12:52:40 +01:00

289 lines
8.9 KiB
JavaScript

beforeAll(() => {
window.CBOR = require('../../node_modules/cbor-js/cbor.js')
require('../../app/assets/javascripts/authenticateSecurityKey.js')
// 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(() => { })
// ensure window.alert() is implemented to simplify errors
jest.spyOn(window, 'alert').mockImplementation(() => { })
})
afterAll(() => {
require('./support/teardown.js')
})
describe('Authenticate with security key', () => {
let button
beforeEach(() => {
document.body.innerHTML = `
<button type="submit" data-module="authenticate-security-key" data-csrf-token="abc123"></button>
`
button = document.querySelector('[data-module="authenticate-security-key"]')
// populate missing values to allow consistent jest.spyOn()
window.fetch = () => { }
window.navigator.credentials = { get: () => { } }
window.alert = () => { }
window.GOVUK.modules.start()
})
afterEach(() => {
// restore window attributes to their original undefined state
delete window.fetch
delete window.navigator.credentials
delete window.alert
})
test('authenticates a credential and redirects based on the admin app response', (done) => {
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url) => {
// initial fetch of options from the server
// fetch defaults to GET
// options from the server are CBOR-encoded
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
return Promise.resolve({
ok: true, arrayBuffer: () => webauthnOptions
})
})
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
expect(options).toEqual('someArbitraryOptions')
// fake PublicKeyCredential response from WebAuthn API
// all of the array properties represent Array(Buffer) objects
const credentialsGetResponse = {
response: {
authenticatorData: [2, 2, 2],
signature: [3, 3, 3],
clientDataJSON: [4, 4, 4]
},
rawId: [1, 1, 1],
type: "public-key",
}
return Promise.resolve(credentialsGetResponse)
})
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url, options = {}) => {
// subsequent POST of credential data to server
const decodedData = window.CBOR.decode(options.body)
expect(decodedData.credentialId).toEqual(new Uint8Array([1, 1, 1]))
expect(decodedData.authenticatorData).toEqual(new Uint8Array([2, 2, 2]))
expect(decodedData.signature).toEqual(new Uint8Array([3, 3, 3]))
expect(decodedData.clientDataJSON).toEqual(new Uint8Array([4, 4, 4]))
expect(options.headers['X-CSRFToken']).toBe('abc123')
const loginResponse = window.CBOR.encode({ redirect_url: '/foo' })
return Promise.resolve({
ok: true, arrayBuffer: () => Promise.resolve(loginResponse)
})
})
jest.spyOn(window.location, 'assign').mockImplementation((href) => {
expect(href).toEqual("/foo")
done();
})
// this will make the test fail if the alert is called
jest.spyOn(window, 'alert').mockImplementation((msg) => {
done(msg)
})
button.click()
});
test('authenticates and passes a redirect url through to the authenticate admin endpoint', (done) => {
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url) => {
// initial fetch of options from the server
// fetch defaults to GET
// options from the server are CBOR-encoded
let webauthnOptions = window.CBOR.encode('someArbitraryOptions')
return Promise.resolve({
ok: true, arrayBuffer: () => webauthnOptions
})
})
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
let credentialsGetResponse = {
response: {
authenticatorData: [],
signature: [],
clientDataJSON: []
},
rawId: [],
type: "public-key",
}
return Promise.resolve(credentialsGetResponse)
})
jest.spyOn(window, 'fetch')
.mockImplementationOnce((url, options = {}) => {
// subsequent POST of credential data to server
expect(url.toString()).toEqual(
'https://www.notifications.service.gov.uk/webauthn/authenticate?next=%2Ffoo%3Fbar%3Dbaz'
);
// mark the test as done here as we've finished all our asserts - if something goes wrong later and
// we end up in the alert mock, that `done(msg)` will override this and mark the test as failed
done();
const loginResponse = window.CBOR.encode({ redirect_url: '/foo' })
return Promise.resolve({
ok: true, arrayBuffer: () => Promise.resolve(loginResponse)
})
})
// make sure we error out if alert is called
jest.spyOn(window, 'alert').mockImplementation((msg) => {
done(msg)
})
button.click()
});
test.each([
['network'],
['server'],
])('alerts if fetching WebAuthn fails (%s error)', (errorType, done) => {
jest.spyOn(window, 'fetch').mockImplementation((_url) => {
if (errorType == 'network') {
return Promise.reject('error')
} else {
return Promise.resolve({ ok: false, statusText: 'error' })
}
})
jest.spyOn(window, 'alert').mockImplementation((msg) => {
expect(msg).toEqual('Error during authentication.\n\nerror')
done()
})
button.click()
})
test('alerts if comms with the authenticator fails', (done) => {
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url) => {
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
return Promise.resolve({
ok: true, arrayBuffer: () => webauthnOptions
})
})
jest.spyOn(window.navigator.credentials, 'get').mockImplementation(() => {
return Promise.reject(new DOMException('error'))
})
jest.spyOn(window, 'alert').mockImplementation((msg) => {
expect(msg).toEqual('Error during authentication.\n\nerror')
done()
})
button.click()
});
test.each([
['network error'],
['internal server error'],
])('alerts if POSTing WebAuthn credentials fails (%s)', (errorType, done) => {
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url) => {
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
return Promise.resolve({
ok: true, arrayBuffer: () => webauthnOptions
})
})
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
expect(options).toEqual('someArbitraryOptions')
const credentialsGetResponse = {
response: {
authenticatorData: [2, 2, 2],
signature: [3, 3, 3],
clientDataJSON: [4, 4, 4]
},
rawId: [1, 1, 1],
type: "public-key",
}
return Promise.resolve(credentialsGetResponse)
})
jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
// subsequent POST of credential data to server
switch (errorType) {
case 'network error':
return Promise.reject('error')
case 'internal server error':
// dont need this becuase we dont cbor return errors
const message = Promise.reject('encoding error')
return Promise.resolve({ ok: false, arrayBuffer: () => message, statusText: 'error' })
}
})
jest.spyOn(window, 'alert').mockImplementation((msg) => {
expect(msg).toEqual('Error during authentication.\n\nerror')
done()
})
button.click()
});
test('reloads page if POSTing WebAuthn credentials returns 403', (done) => {
jest.spyOn(window, 'fetch')
.mockImplementationOnce((_url) => {
const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
return Promise.resolve({
ok: true, arrayBuffer: () => webauthnOptions
})
})
jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
expect(options).toEqual('someArbitraryOptions')
const credentialsGetResponse = {
response: {
authenticatorData: [2, 2, 2],
signature: [3, 3, 3],
clientDataJSON: [4, 4, 4]
},
rawId: [1, 1, 1],
type: "public-key",
}
return Promise.resolve(credentialsGetResponse)
})
jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
return Promise.resolve(
{
ok: false,
status: 403,
})
})
// assert that reload is called and the page is refreshed
jest.spyOn(window.location, 'reload').mockImplementation(() => {
done();
})
// this will make the test fail if the alert is called
jest.spyOn(window, 'alert').mockImplementation((msg) => {
done(msg)
})
button.click()
});
});