Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import sinon from 'sinon';
import quibble from 'quibble';
import { waitFor } from '@testing-library/dom';
import type { IsWebauthnSupported } from './is-webauthn-supported';
import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported';
import type { IsWebauthnPlatformSupported } from './is-webauthn-platform-supported';

describe('WebauthnInputElement', () => {
const isWebauthnSupported = sinon.stub<
Expand All @@ -14,25 +12,18 @@ describe('WebauthnInputElement', () => {
Parameters<IsWebauthnPasskeySupported>,
ReturnType<IsWebauthnPasskeySupported>
>();
const isWebauthnPlatformSupported = sinon.stub<
Parameters<IsWebauthnPlatformSupported>,
ReturnType<IsWebauthnPlatformSupported>
>();

before(async () => {
quibble('./is-webauthn-supported', isWebauthnSupported);
quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported);
quibble('./is-webauthn-platform-supported', isWebauthnPlatformSupported);
await import('./webauthn-input-element');
});

beforeEach(() => {
isWebauthnSupported.reset();
isWebauthnSupported.returns(false);
isWebauthnPasskeySupported.reset();
isWebauthnPasskeySupported.resolves(false);
isWebauthnPlatformSupported.reset();
isWebauthnPlatformSupported.resolves(false);
isWebauthnPasskeySupported.returns(false);
});

after(() => {
Expand Down Expand Up @@ -62,70 +53,50 @@ describe('WebauthnInputElement', () => {
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
});

it('becomes visible', async () => {
it('becomes visible', () => {
const element = document.querySelector('lg-webauthn-input')!;

await waitFor(() => expect(element.hidden).to.be.false());
expect(element.hidden).to.be.false();
});
});

context('input for platform authenticator', () => {
context('device does not have available platform authenticator', () => {
beforeEach(() => {
isWebauthnPlatformSupported.resolves(false);
document.body.innerHTML = `<lg-webauthn-input platform hidden></lg-webauthn-input>`;
});

it('stays hidden', async () => {
const element = document.querySelector('lg-webauthn-input')!;

await waitFor(() => expect(element.isInitialized).to.be.true());

expect(element.hidden).to.be.true();
});
});

context('device has available platform authenticator', () => {
context('no passkey only restriction', () => {
beforeEach(() => {
isWebauthnPlatformSupported.resolves(true);
document.body.innerHTML = `<lg-webauthn-input platform hidden></lg-webauthn-input>`;
});

it('becomes visible', async () => {
it('becomes visible', () => {
const element = document.querySelector('lg-webauthn-input')!;

await waitFor(() => expect(element.hidden).to.be.false());
expect(element.hidden).to.be.false();
});
});

context('passkey supported only', () => {
context('device does not support passkey', () => {
beforeEach(() => {
isWebauthnPlatformSupported.resolves(true);
isWebauthnPasskeySupported.returns(false);
document.body.innerHTML = `<lg-webauthn-input platform passkey-supported-only hidden></lg-webauthn-input>`;
});

it('stays hidden', async () => {
it('stays hidden', () => {
const element = document.querySelector('lg-webauthn-input')!;

await waitFor(() => expect(element.isInitialized).to.be.true());

expect(element.hidden).to.be.true();
});
});

context('device supports passkey', () => {
beforeEach(() => {
isWebauthnPlatformSupported.resolves(true);
isWebauthnPasskeySupported.returns(true);
document.body.innerHTML = `<lg-webauthn-input platform passkey-supported-only hidden></lg-webauthn-input>`;
});

it('becomes visible', async () => {
it('becomes visible', () => {
const element = document.querySelector('lg-webauthn-input')!;

await waitFor(() => expect(element.hidden).to.be.false());
expect(element.hidden).to.be.false();
});
});
});
Expand Down
24 changes: 6 additions & 18 deletions app/javascript/packages/webauthn/webauthn-input-element.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import isWebauthnPasskeySupported from './is-webauthn-passkey-supported';
import isWebauthnPlatformSupported from './is-webauthn-platform-supported';
import isWebauthnSupported from './is-webauthn-supported';

export class WebauthnInputElement extends HTMLElement {
isInitialized = false;

async connectedCallback() {
await this.toggleVisibleIfSupported();
this.isInitialized = true;
connectedCallback() {
this.toggleVisibleIfSupported();
}

get isPlatform(): boolean {
Expand All @@ -18,24 +14,16 @@ export class WebauthnInputElement extends HTMLElement {
return this.hasAttribute('passkey-supported-only');
}

async isSupported(): Promise<boolean> {
isSupported(): boolean {
if (!isWebauthnSupported()) {
return false;
}

if (!this.isPlatform) {
return true;
}

if (!(await isWebauthnPlatformSupported())) {
return false;
}

return !this.isOnlyPasskeySupported || isWebauthnPasskeySupported();
return !this.isPlatform || !this.isOnlyPasskeySupported || isWebauthnPasskeySupported();
}

async toggleVisibleIfSupported() {
if (await this.isSupported()) {
toggleVisibleIfSupported() {
if (this.isSupported()) {
this.removeAttribute('hidden');
}
}
Expand Down
18 changes: 1 addition & 17 deletions app/javascript/packs/webauthn-authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ function webauthn() {
const webauthAlertContainer = document.querySelector('.usa-alert--error')!;
const webauthnPlatformRequested =
webauthnInProgressContainer.dataset.platformAuthenticatorRequested === 'true';
const multipleFactorsEnabled =
webauthnInProgressContainer.dataset.multipleFactorsEnabled === 'true';
const isPlatformAvailable =
(document.getElementById('webauthn_device') as HTMLInputElement).value === 'true';

const spinner = document.getElementById('spinner')!;
spinner.classList.remove('display-none');
Expand All @@ -20,10 +16,7 @@ function webauthn() {
(document.getElementById('credentials') as HTMLInputElement).value,
);

if (
!isWebauthnSupported() ||
(webauthnPlatformRequested && !isPlatformAvailable && !multipleFactorsEnabled)
) {
if (!isWebauthnSupported()) {
const href = webauthnInProgressContainer.getAttribute('data-webauthn-not-enabled-url')!;
window.location.href = href;
} else {
Expand Down Expand Up @@ -60,13 +53,4 @@ function webauthnButton() {
button.addEventListener('click', webauthn);
}

function isPlatformAuthenticatorAvailable() {
return window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable().then(
(result) => {
(document.getElementById('webauthn_device') as HTMLInputElement).value = String(result);
},
);
}

document.addEventListener('DOMContentLoaded', webauthnButton);
document.addEventListener('DOMContentLoaded', isPlatformAuthenticatorAvailable);
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,13 @@
<%= hidden_field_tag :signature, '', id: 'signature' %>
<%= hidden_field_tag :client_data_json, '', id: 'client_data_json' %>
<%= hidden_field_tag :webauthn_error, '', id: 'webauthn_error' %>
<%= hidden_field_tag :platform, '', id: 'platform' %>
<%= hidden_field_tag :webauthn_device, '', id: 'webauthn_device' %>
<%= hidden_field_tag :platform, @presenter.platform_authenticator?, id: 'platform' %>

<%= content_tag(
:div,
id: 'webauthn-auth-in-progress',
data: {
webauthn_not_enabled_url: @presenter.webauthn_not_enabled_link,
platform_authenticator_requested: @presenter.platform_authenticator?,
multiple_factors_enabled: @presenter.multiple_factors_enabled?,
},
) do %>
<div class="display-none spinner text-center margin-bottom-4" id="spinner">
Expand Down
32 changes: 0 additions & 32 deletions spec/features/two_factor_authentication/sign_in_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -544,16 +544,6 @@ def attempt_to_bypass_2fa
allow(IdentityConfig.store).to receive(:platform_auth_set_up_enabled).and_return(true)
end

it 'shows options with webauthn visible', :js, driver: :headless_chrome_mobile do
sign_in_user(webauthn_configuration.user)

click_link t('two_factor_authentication.login_options_link_text')
simulate_platform_authenticator_available

expect(page).
to have_content t('two_factor_authentication.login_options.webauthn_platform')
end

it 'allows user to be signed in without issue' do
mock_webauthn_verification_challenge

Expand All @@ -570,16 +560,6 @@ def attempt_to_bypass_2fa
allow(IdentityConfig.store).to receive(:platform_auth_set_up_enabled).and_return(false)
end

it 'shows options with webauthn visible', :js, driver: :headless_chrome_mobile do
sign_in_user(webauthn_configuration.user)

click_link t('two_factor_authentication.login_options_link_text')
simulate_platform_authenticator_available

expect(page).
to have_content t('two_factor_authentication.login_options.webauthn_platform')
end

it 'allows user to be signed in without issue' do
mock_webauthn_verification_challenge

Expand All @@ -590,18 +570,6 @@ def attempt_to_bypass_2fa
expect(page).to have_current_path(account_path)
end
end

def simulate_platform_authenticator_available
# Since the element will have already initialized by this point and it can't be guaranteed
# that isUserVerifyingPlatformAuthenticatorAvailable would yield the expected value, stub
# and reconnect the elements to simulate as if it were the expected value on page load.
page.evaluate_script(<<~JS)
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true);
JS
page.evaluate_script(<<~JS)
document.querySelectorAll('lg-webauthn-input').forEach((input) => input.connectedCallback());
JS
end
end
end

Expand Down
Loading