summaryrefslogtreecommitdiff
path: root/spec/frontend/authentication/webauthn/authenticate_spec.js
blob: 8b27560bbbee9d303745d6f983bef9af9a8377bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
import MockWebAuthnDevice from './mock_webauthn_device';
import { useMockNavigatorCredentials } from './util';

const mockResponse = {
  type: 'public-key',
  id: '',
  rawId: '',
  response: { clientDataJSON: '', authenticatorData: '', signature: '', userHandle: '' },
  getClientExtensionResults: () => {},
};

describe('WebAuthnAuthenticate', () => {
  useMockNavigatorCredentials();

  let fallbackElement;
  let webAuthnDevice;
  let container;
  let component;
  let submitSpy;

  const findDeviceResponseInput = () => container[0].querySelector('#js-device-response');
  const findDeviceResponseInputValue = () => findDeviceResponseInput().value;
  const findMessage = () => container[0].querySelector('p');
  const findRetryButton = () => container[0].querySelector('#js-token-2fa-try-again');
  const expectAuthenticated = () => {
    expect(container.text()).toMatchInterpolatedText(
      'We heard back from your device. You have been authenticated.',
    );
    expect(findDeviceResponseInputValue()).toBe(JSON.stringify(mockResponse));
    expect(submitSpy).toHaveBeenCalled();
  };

  beforeEach(() => {
    loadFixtures('webauthn/authenticate.html');
    fallbackElement = document.createElement('div');
    fallbackElement.classList.add('js-2fa-form');
    webAuthnDevice = new MockWebAuthnDevice();
    container = $('#js-authenticate-token-2fa');
    component = new WebAuthnAuthenticate(
      container,
      '#js-login-token-2fa-form',
      {
        options:
          // we need some valid base64 for base64ToBuffer
          // so we use "YQ==" = base64("a")
          JSON.stringify({
            challenge: 'YQ==',
            timeout: 120000,
            allowCredentials: [
              { type: 'public-key', id: 'YQ==' },
              { type: 'public-key', id: 'YQ==' },
            ],
            userVerification: 'discouraged',
          }),
      },
      document.querySelector('#js-login-2fa-device'),
      fallbackElement,
    );
    submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
  });

  describe('with webauthn unavailable', () => {
    let oldGetCredentials;

    beforeEach(() => {
      oldGetCredentials = window.navigator.credentials.get;
      window.navigator.credentials.get = null;
    });

    afterEach(() => {
      window.navigator.credentials.get = oldGetCredentials;
    });

    it('falls back to normal 2fa', () => {
      component.start();

      expect(container.html()).toBe('');
      expect(container[0]).toHaveClass('hidden');
      expect(fallbackElement).not.toHaveClass('hidden');
    });
  });

  describe('with webauthn available', () => {
    beforeEach(() => {
      component.start();
    });

    it('shows in progress', () => {
      const inProgressMessage = container.find('p');

      expect(inProgressMessage.text()).toMatchInterpolatedText(
        "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.",
      );
    });

    it('allows authenticating via a WebAuthn device', () => {
      webAuthnDevice.respondToAuthenticateRequest(mockResponse);

      return waitForPromises().then(() => {
        expectAuthenticated();
      });
    });

    describe('errors', () => {
      beforeEach(() => {
        webAuthnDevice.rejectAuthenticateRequest(new DOMException());

        return waitForPromises();
      });

      it('displays an error message', () => {
        expect(submitSpy).not.toHaveBeenCalled();
        expect(findMessage().textContent).toMatchInterpolatedText(
          'There was a problem communicating with your device. (Error)',
        );
      });

      it('allows retrying authentication after an error', () => {
        findRetryButton().click();
        webAuthnDevice.respondToAuthenticateRequest(mockResponse);

        return waitForPromises().then(() => {
          expectAuthenticated();
        });
      });
    });
  });
});