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
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ def confirm
handle_webauthn_result(result)
end

def error; end

private

def handle_webauthn_result(result)
Expand Down
1 change: 0 additions & 1 deletion app/javascript/packages/webauthn/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { default as isWebauthnSupported } from './is-webauthn-supported';
export { default as enrollWebauthnDevice } from './enroll-webauthn-device';
export { default as extractCredentials } from './extract-credentials';
export { default as verifyWebauthnDevice } from './verify-webauthn-device';
Expand Down
32 changes: 0 additions & 32 deletions app/javascript/packages/webauthn/is-webauthn-supported.spec.ts

This file was deleted.

5 changes: 0 additions & 5 deletions app/javascript/packages/webauthn/is-webauthn-supported.ts

This file was deleted.

70 changes: 19 additions & 51 deletions app/javascript/packages/webauthn/webauthn-input-element.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,38 @@
import sinon from 'sinon';
import quibble from 'quibble';
import type { IsWebauthnSupported } from './is-webauthn-supported';
import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported';

describe('WebauthnInputElement', () => {
const isWebauthnSupported = sinon.stub<
Parameters<IsWebauthnSupported>,
ReturnType<IsWebauthnSupported>
>();
const isWebauthnPasskeySupported = sinon.stub<
Parameters<IsWebauthnPasskeySupported>,
ReturnType<IsWebauthnPasskeySupported>
>();

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

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

after(() => {
quibble.reset();
});

context('browser does not support webauthn', () => {
context('input for non-platform authenticator', () => {
beforeEach(() => {
isWebauthnSupported.returns(false);
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
});

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

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

context('browser supports webauthn', () => {
beforeEach(() => {
isWebauthnSupported.returns(true);
});

context('input for non-platform authenticator', () => {
context('input for platform authenticator', () => {
context('no passkey only restriction', () => {
beforeEach(() => {
document.body.innerHTML = `<lg-webauthn-input hidden></lg-webauthn-input>`;
document.body.innerHTML = `<lg-webauthn-input platform hidden></lg-webauthn-input>`;
});

it('becomes visible', () => {
Expand All @@ -60,44 +42,30 @@ describe('WebauthnInputElement', () => {
});
});

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

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

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

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

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

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

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

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

expect(element.hidden).to.be.false();
});
expect(element.hidden).to.be.false();
});
});
});
Expand Down
5 changes: 0 additions & 5 deletions app/javascript/packages/webauthn/webauthn-input-element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import isWebauthnPasskeySupported from './is-webauthn-passkey-supported';
import isWebauthnSupported from './is-webauthn-supported';

export class WebauthnInputElement extends HTMLElement {
connectedCallback() {
Expand All @@ -15,10 +14,6 @@ export class WebauthnInputElement extends HTMLElement {
}

isSupported(): boolean {
if (!isWebauthnSupported()) {
return false;
}

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

Expand Down
45 changes: 19 additions & 26 deletions app/javascript/packs/webauthn-authenticate.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
import { isWebauthnSupported, verifyWebauthnDevice } from '@18f/identity-webauthn';
import { verifyWebauthnDevice } from '@18f/identity-webauthn';
import type { VerifyCredentialDescriptor } from '@18f/identity-webauthn';

function webauthn() {
const webauthnInProgressContainer = document.getElementById('webauthn-auth-in-progress')!;

const spinner = document.getElementById('spinner')!;
spinner.classList.remove('display-none');

const credentials: VerifyCredentialDescriptor[] = JSON.parse(
(document.getElementById('credentials') as HTMLInputElement).value,
);

if (!isWebauthnSupported()) {
const href = webauthnInProgressContainer.getAttribute('data-webauthn-not-enabled-url')!;
window.location.href = href;
} else {
// if platform auth is not supported on device, we should take user to the error screen if theres no additional methods.
verifyWebauthnDevice({
userChallenge: (document.getElementById('user_challenge') as HTMLInputElement).value,
credentials,
// if platform auth is not supported on device, we should take user to the error screen if theres no additional methods.
verifyWebauthnDevice({
userChallenge: (document.getElementById('user_challenge') as HTMLInputElement).value,
credentials,
})
.then((result) => {
(document.getElementById('credential_id') as HTMLInputElement).value = result.credentialId;
(document.getElementById('authenticator_data') as HTMLInputElement).value =
result.authenticatorData;
(document.getElementById('client_data_json') as HTMLInputElement).value =
result.clientDataJSON;
(document.getElementById('signature') as HTMLInputElement).value = result.signature;
})
.catch((error: Error) => {
(document.getElementById('webauthn_error') as HTMLInputElement).value = error.name;
})
.then((result) => {
(document.getElementById('credential_id') as HTMLInputElement).value = result.credentialId;
(document.getElementById('authenticator_data') as HTMLInputElement).value =
result.authenticatorData;
(document.getElementById('client_data_json') as HTMLInputElement).value =
result.clientDataJSON;
(document.getElementById('signature') as HTMLInputElement).value = result.signature;
})
.catch((error: Error) => {
(document.getElementById('webauthn_error') as HTMLInputElement).value = error.name;
})
.then(() => {
(document.getElementById('webauthn_form') as HTMLFormElement).submit();
});
}
.then(() => {
(document.getElementById('webauthn_form') as HTMLFormElement).submit();
});
}

function webauthnButton() {
Expand Down
10 changes: 1 addition & 9 deletions app/javascript/packs/webauthn-setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
isWebauthnSupported,
enrollWebauthnDevice,
extractCredentials,
longToByteArray,
} from '@18f/identity-webauthn';
import { enrollWebauthnDevice, extractCredentials, longToByteArray } from '@18f/identity-webauthn';
import { forceRedirect } from '@18f/identity-url';
import type { Navigate } from '@18f/identity-url';

Expand Down Expand Up @@ -32,9 +27,6 @@ export function reloadWithError(
}

function webauthn() {
if (!isWebauthnSupported()) {
reloadWithError('NotSupportedError');
}
const form = document.getElementById('webauthn_form') as HTMLFormElement;
form.addEventListener('submit', (event) => {
event.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,6 @@ def cancel_link
end
end

def webauthn_not_enabled_link
if platform_authenticator?
login_two_factor_webauthn_error_path
else
login_two_factor_options_path
end
end

def multiple_factors_enabled?
service_provider_mfa_policy.multiple_factors_enabled?
end
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,21 @@
<%= hidden_field_tag :webauthn_error, '', id: 'webauthn_error' %>
<%= hidden_field_tag :platform, @presenter.platform_authenticator? %>

<%= content_tag(
:div,
id: 'webauthn-auth-in-progress',
data: {
webauthn_not_enabled_url: @presenter.webauthn_not_enabled_link,
},
) do %>
<div class="display-none spinner text-center margin-bottom-5" id="spinner">
<%= image_tag(
asset_url('spinner.gif'),
srcset: asset_url('spinner@2x.gif'),
height: 144,
width: 144,
alt: '',
) %>
<p class="text-bold margin-y-5">
<%= t('two_factor_authentication.webauthn_authenticating') %>
</p>
</div>
<button id="webauthn-button" class="display-block margin-y-3 usa-button usa-button--big usa-button--wide">
<%= @presenter.authenticate_button_text %>
</button>
<% end %>
<div class="display-none spinner text-center margin-bottom-5" id="spinner">
<%= image_tag(
asset_url('spinner.gif'),
srcset: asset_url('spinner@2x.gif'),
height: 144,
width: 144,
alt: '',
) %>
<p class="text-bold margin-y-5">
<%= t('two_factor_authentication.webauthn_authenticating') %>
</p>
</div>
<button id="webauthn-button" class="display-block margin-y-3 usa-button usa-button--big usa-button--wide">
<%= @presenter.authenticate_button_text %>
</button>

<%= f.input(
:remember_device,
Expand Down
3 changes: 0 additions & 3 deletions config/locales/two_factor_authentication/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,7 @@ en:
webauthn_authenticating: Authenticating your credentials…
webauthn_error:
additional_methods_link: choose another authentication method
error_page_text: You have face or touch unlock enabled for this account. Use the
same device and browser profile each time.
multiple_methods: Face or touch unlock was unsuccessful. Please try again or %{link}.
title: We can’t identify your device
webauthn_header_text: Connect your security key
webauthn_piv_available: Use your PIV or CAC
webauthn_platform_header_text: Use face or touch unlock
Expand Down
3 changes: 0 additions & 3 deletions config/locales/two_factor_authentication/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,8 @@ es:
webauthn_authenticating: Autenticando sus credenciales…
webauthn_error:
additional_methods_link: elija otro método de autenticación
error_page_text: Tiene habilitado el desbloqueo facial o táctil para esta
cuenta. Utilice cada vez el mismo dispositivo y perfil de navegador.
multiple_methods: El desbloqueo facial o táctil no fue exitoso. Por favor,
inténtelo de nuevo o %{link}.
title: No podemos reconocer su dispositivo
webauthn_header_text: Conecte su llave de seguridad
webauthn_piv_available: Utilice su PIV o CAC
webauthn_platform_header_text: Usar desbloqueo facial o táctil
Expand Down
4 changes: 0 additions & 4 deletions config/locales/two_factor_authentication/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,8 @@ fr:
webauthn_authenticating: Authentification de vos informations d’identification…
webauthn_error:
additional_methods_link: choisir une autre méthode d’authentification
error_page_text: Vous avez activé le déverrouillage facial ou tactile pour ce
compte. Utilisez le même appareil et le même profil de navigateur chaque
fois.
multiple_methods: Le déverrouillage facial ou tactile n’a pas fonctionné.
Veuillez réessayer ou %{link}.
title: Nous ne pouvons pas identifier votre appareil
webauthn_header_text: Connectez votre clé de sécurité
webauthn_piv_available: Utilisez votre PIV ou CAC
webauthn_platform_header_text: Utilisez le déverrouillage facial ou tactile
Expand Down
1 change: 0 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@
get '/login/two_factor/piv_cac' => 'two_factor_authentication/piv_cac_verification#show'
get '/login/two_factor/piv_cac/present_piv_cac' => 'two_factor_authentication/piv_cac_verification#redirect_to_piv_cac_service'
get '/login/two_factor/webauthn' => 'two_factor_authentication/webauthn_verification#show'
get '/login/two_factor/webauthn_error' => 'two_factor_authentication/webauthn_verification#error'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should have removed this in a separate deployment, to avoid 50/50 issues. Pretty low risk though, since this error would only be seen by people using unsupported browsers. I'll check the logs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After checking logs, there's some traffic to this page still, but reasonably low overall.

Also worth noting the actual experience for someone on an unsupported browser should be marginally improved by these changes, since they'll still see an error message when trying to authenticate. Unlike before, they'll now have the option to select another MFA method on the unsupported device, which was not possible previously.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be on the safe side: #8773

patch '/login/two_factor/webauthn' => 'two_factor_authentication/webauthn_verification#confirm'
get 'login/two_factor/backup_code' => 'two_factor_authentication/backup_code_verification#show'
post 'login/two_factor/backup_code' => 'two_factor_authentication/backup_code_verification#create'
Expand Down