From 6bd5a800d4a3083b300864a0e195ebacfafcf9d3 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Thu, 16 Nov 2023 10:57:31 -0500 Subject: [PATCH 01/13] ft unlock support fix --- app/javascript/packages/webauthn/enroll-webauthn-device.ts | 4 ++++ .../packages/webauthn/is-webauthn-passkey-supported.ts | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/javascript/packages/webauthn/enroll-webauthn-device.ts b/app/javascript/packages/webauthn/enroll-webauthn-device.ts index ffbae3a19cf..b85bbad7602 100644 --- a/app/javascript/packages/webauthn/enroll-webauthn-device.ts +++ b/app/javascript/packages/webauthn/enroll-webauthn-device.ts @@ -94,6 +94,10 @@ async function enrollWebauthnDevice({ }, })) as PublicKeyCredential; + if (PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) { + + }; + const response = credential.response as AuthenticatorAttestationResponseBrowserSupport; const authenticatorData = response.getAuthenticatorData?.(); const authenticatorDataFlagsValue = authenticatorData diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index b496028322b..344a47a976f 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -24,7 +24,11 @@ function isQualifyingAndroidDevice(): boolean { ); } +function isCredentialSupported(): boolean { + return this.window.PasswordCredential || this.window.FederatedCredential +} + const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - isQualifyingIOSDevice() || isQualifyingAndroidDevice(); + (isQualifyingIOSDevice() || isQualifyingAndroidDevice()) && isCredentialSupported() ; export default isWebauthnPasskeySupported; From 3ad7bfadba0a481f04dc62487a08330804dc5c07 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Thu, 16 Nov 2023 12:38:55 -0500 Subject: [PATCH 02/13] ensure its valid --- .../packages/webauthn/is-webauthn-passkey-supported.ts | 6 +----- app/javascript/packages/webauthn/webauthn-input-element.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index 344a47a976f..93c2dd9b190 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -24,11 +24,7 @@ function isQualifyingAndroidDevice(): boolean { ); } -function isCredentialSupported(): boolean { - return this.window.PasswordCredential || this.window.FederatedCredential -} - const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - (isQualifyingIOSDevice() || isQualifyingAndroidDevice()) && isCredentialSupported() ; + (isQualifyingIOSDevice() || isQualifyingAndroidDevice()); export default isWebauthnPasskeySupported; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 11938ab51a6..09a35cbfd27 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -13,12 +13,12 @@ export class WebauthnInputElement extends HTMLElement { return this.hasAttribute('show-unsupported-passkey'); } - toggleVisibleIfPasskeySupported() { + async toggleVisibleIfPasskeySupported() { if (!this.hasAttribute('hidden')) { return; } - if (isWebauthnPasskeySupported()) { + if (isWebauthnPasskeySupported() && window.PublicKeyCredential && window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From b33381ed6caa6531007003a5013bbfc2be1a61ce Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Thu, 16 Nov 2023 13:08:46 -0500 Subject: [PATCH 03/13] changelog: User-Facing Improvements, Authentication, Only show F/t Unlock when computer is eligible --- .../packages/webauthn/webauthn-input-element.ts | 8 ++++++-- db/schema.rb | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 09a35cbfd27..15b0972fd9c 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -13,12 +13,16 @@ export class WebauthnInputElement extends HTMLElement { return this.hasAttribute('show-unsupported-passkey'); } - async toggleVisibleIfPasskeySupported() { + async isPublicKeyCredentialSupported() { + return window.PublicKeyCredential && await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() + } + + toggleVisibleIfPasskeySupported() { if (!this.hasAttribute('hidden')) { return; } - if (isWebauthnPasskeySupported() && window.PublicKeyCredential && window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) { + if (isWebauthnPasskeySupported() && this.isPublicKeyCredentialSupported()) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; diff --git a/db/schema.rb b/db/schema.rb index fceaf6e546f..47f1cce754a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -629,8 +629,6 @@ t.datetime "updated_at", precision: nil, null: false t.datetime "bounced_at", precision: nil t.datetime "reminder_sent_at", precision: nil - t.datetime "expiration_notice_sent_at", precision: nil - t.index ["expiration_notice_sent_at"], name: "index_usps_confirmation_codes_on_expiration_notice_sent_at" t.index ["otp_fingerprint"], name: "index_usps_confirmation_codes_on_otp_fingerprint" t.index ["profile_id"], name: "index_usps_confirmation_codes_on_profile_id" t.index ["reminder_sent_at"], name: "index_usps_confirmation_codes_on_reminder_sent_at" @@ -660,7 +658,7 @@ add_foreign_key "iaa_gtcs", "partner_accounts" add_foreign_key "iaa_orders", "iaa_gtcs" add_foreign_key "in_person_enrollments", "profiles" - add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer" + add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer", validate: false add_foreign_key "in_person_enrollments", "users" add_foreign_key "integration_usages", "iaa_orders" add_foreign_key "integration_usages", "integrations" From e7c1a75fd998aeb4e9bab2db8737cdf1099e90fe Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Thu, 16 Nov 2023 15:12:55 -0500 Subject: [PATCH 04/13] fix spec --- app/javascript/packages/webauthn/enroll-webauthn-device.ts | 4 ---- .../packages/webauthn/is-webauthn-passkey-supported.ts | 2 +- app/javascript/packages/webauthn/webauthn-input-element.ts | 5 ++++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/javascript/packages/webauthn/enroll-webauthn-device.ts b/app/javascript/packages/webauthn/enroll-webauthn-device.ts index b85bbad7602..ffbae3a19cf 100644 --- a/app/javascript/packages/webauthn/enroll-webauthn-device.ts +++ b/app/javascript/packages/webauthn/enroll-webauthn-device.ts @@ -94,10 +94,6 @@ async function enrollWebauthnDevice({ }, })) as PublicKeyCredential; - if (PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) { - - }; - const response = credential.response as AuthenticatorAttestationResponseBrowserSupport; const authenticatorData = response.getAuthenticatorData?.(); const authenticatorDataFlagsValue = authenticatorData diff --git a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts index 93c2dd9b190..b496028322b 100644 --- a/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts +++ b/app/javascript/packages/webauthn/is-webauthn-passkey-supported.ts @@ -25,6 +25,6 @@ function isQualifyingAndroidDevice(): boolean { } const isWebauthnPasskeySupported: IsWebauthnPasskeySupported = () => - (isQualifyingIOSDevice() || isQualifyingAndroidDevice()); + isQualifyingIOSDevice() || isQualifyingAndroidDevice(); export default isWebauthnPasskeySupported; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 15b0972fd9c..6c05c7d9212 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -14,7 +14,10 @@ export class WebauthnInputElement extends HTMLElement { } async isPublicKeyCredentialSupported() { - return window.PublicKeyCredential && await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() + return ( + window.PublicKeyCredential && + (await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) + ); } toggleVisibleIfPasskeySupported() { From fe8ba4a9a3bb8f338bba0bdbe473dd8564e98866 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Fri, 17 Nov 2023 11:07:24 -0500 Subject: [PATCH 05/13] refactor to make method async --- .../packages/webauthn/webauthn-input-element.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 6c05c7d9212..b26ccdcf41e 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -13,19 +13,14 @@ export class WebauthnInputElement extends HTMLElement { return this.hasAttribute('show-unsupported-passkey'); } - async isPublicKeyCredentialSupported() { - return ( - window.PublicKeyCredential && - (await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()) - ); - } - - toggleVisibleIfPasskeySupported() { + async toggleVisibleIfPasskeySupported() { if (!this.hasAttribute('hidden')) { return; } - - if (isWebauthnPasskeySupported() && this.isPublicKeyCredentialSupported()) { + const isPublicKeyCredentialSupported = + window.PublicKeyCredential && + (await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()); + if (isWebauthnPasskeySupported() && isPublicKeyCredentialSupported) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From a37cd4db55092737532b3ec334556a8d118ccaf3 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Mon, 20 Nov 2023 13:22:21 -0500 Subject: [PATCH 06/13] fix javascript test --- .../webauthn/webauthn-input-element.spec.ts | 49 ++++++++++++++++--- app/javascript/packs/webauthn-setup.ts | 5 +- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 7c6ee1a0ffb..2c06afef16f 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,5 +1,6 @@ import sinon from 'sinon'; import quibble from 'quibble'; +import { useSandbox, useDefineProperty } from '@18f/identity-test-helpers'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; describe('WebauthnInputElement', () => { @@ -8,6 +9,9 @@ describe('WebauthnInputElement', () => { ReturnType >(); + const defineProperty = useDefineProperty(); + const sandbox = useSandbox(); + before(async () => { quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); await import('./webauthn-input-element'); @@ -20,6 +24,12 @@ describe('WebauthnInputElement', () => { context('device does not support passkey', () => { context('unsupported passkey not shown', () => { beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { + isUserVerifyingPlatformAuthenticatorAvailable: sandbox.stub().resolves(true), + }, + }); isWebauthnPasskeySupported.returns(false); document.body.innerHTML = ``; }); @@ -47,15 +57,42 @@ describe('WebauthnInputElement', () => { }); context('device supports passkey', () => { - beforeEach(() => { - isWebauthnPasskeySupported.returns(true); - document.body.innerHTML = ``; + context('unsupported publickeycredential not shown', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: undefined, + }); + + isWebauthnPasskeySupported.returns(true); + document.body.innerHTML = ``; + }); + + it('becomes visible', () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.true(); + }); }); - it('becomes visible', () => { - const element = document.querySelector('lg-webauthn-input')!; + context('publickeycredential input is shown', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { + isUserVerifyingPlatformAuthenticatorAvailable: sandbox.stub().resolves(true), + }, + }); + + isWebauthnPasskeySupported.returns(true); + document.body.innerHTML = ``; + }); - expect(element.hidden).to.be.false(); + it('becomes visible', () => { + const element = document.querySelector('lg-webauthn-input')!; + + expect(element.hidden).to.be.false(); + }); }); }); }); diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index 614335d8d23..7195ea4093c 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -64,8 +64,9 @@ function webauthn() { (document.getElementById('client_data_json') as HTMLInputElement).value = result.clientDataJSON; if (result.authenticatorDataFlagsValue) { - (document.getElementById('authenticator_data_value') as HTMLInputElement).value = - `${result.authenticatorDataFlagsValue}`; + ( + document.getElementById('authenticator_data_value') as HTMLInputElement + ).value = `${result.authenticatorDataFlagsValue}`; } if (result.transports) { (document.getElementById('transports') as HTMLInputElement).value = From 77b2bf71371a8e32a4ffe80911dfe62e2f91a555 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Mon, 20 Nov 2023 13:50:57 -0500 Subject: [PATCH 07/13] fix schema --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 47f1cce754a..789767c2929 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -658,7 +658,7 @@ add_foreign_key "iaa_gtcs", "partner_accounts" add_foreign_key "iaa_orders", "iaa_gtcs" add_foreign_key "in_person_enrollments", "profiles" - add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer", validate: false + add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer" add_foreign_key "in_person_enrollments", "users" add_foreign_key "integration_usages", "iaa_orders" add_foreign_key "integration_usages", "integrations" From b1011d8e72b00c310e54a5c3043e367008e3e1db Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Mon, 20 Nov 2023 14:45:34 -0500 Subject: [PATCH 08/13] add public key credential supported in separate file --- ...is-public-key-credential-supported.spec.ts | 45 +++++++++++++++++++ .../is-public-key-credential-supported.ts | 7 +++ .../webauthn/webauthn-input-element.ts | 7 ++- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts create mode 100644 app/javascript/packages/webauthn/is-public-key-credential-supported.ts diff --git a/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts b/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts new file mode 100644 index 00000000000..c1a983e7b80 --- /dev/null +++ b/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts @@ -0,0 +1,45 @@ +import { useDefineProperty } from '@18f/identity-test-helpers'; +import isPublicKeyCredentialSupported from './is-public-key-credential-supported'; + +describe('isPublicKeyCredentialSupported', () => { + const defineProperty = useDefineProperty(); + + context('public key credential exists', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(true) }, + }); + }); + + it('resolves to true', () => { + expect(isPublicKeyCredentialSupported()).to.equal(true); + }); + + context('isUserVerifyingPlatformAuthenticatorAvailable is set to false', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(false) }, + }); + }); + + it('resolves to false', () => { + expect(isPublicKeyCredentialSupported()).to.equal(false); + }); + }); + }); + + context('public key credential does not exist ', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: undefined, + }); + }); + + it('resolves to false', () => { + expect(isPublicKeyCredentialSupported()).to.equal(false); + }); + }); +}); diff --git a/app/javascript/packages/webauthn/is-public-key-credential-supported.ts b/app/javascript/packages/webauthn/is-public-key-credential-supported.ts new file mode 100644 index 00000000000..a1e301d108e --- /dev/null +++ b/app/javascript/packages/webauthn/is-public-key-credential-supported.ts @@ -0,0 +1,7 @@ +async function isPublicKeyCredentialSupported(): Promise { + const isUserVerifyingPlatformAuthenticatorAvailable = + await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + return window.PublicKeyCredential && isUserVerifyingPlatformAuthenticatorAvailable; +} + +export default isPublicKeyCredentialSupported; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index b26ccdcf41e..bc74d0fdb74 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,4 +1,5 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; +import isPublicKeyCredentialSupported from './is-public-key-credential-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { @@ -17,10 +18,8 @@ export class WebauthnInputElement extends HTMLElement { if (!this.hasAttribute('hidden')) { return; } - const isPublicKeyCredentialSupported = - window.PublicKeyCredential && - (await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()); - if (isWebauthnPasskeySupported() && isPublicKeyCredentialSupported) { + + if (isWebauthnPasskeySupported() && (await isPublicKeyCredentialSupported())) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From 8ae821a75181da1498c3d74ee7c344acbd3e59e6 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Mon, 20 Nov 2023 16:52:29 -0500 Subject: [PATCH 09/13] update platform auth available spec --- ...is-public-key-credential-supported.spec.ts | 45 ------------------ .../is-public-key-credential-supported.ts | 7 --- ...n-platform-authenticator-available.spec.ts | 47 +++++++++++++++++++ ...bauthn-platform-authenticator-available.ts | 6 +++ .../webauthn/webauthn-input-element.spec.ts | 32 ++++--------- .../webauthn/webauthn-input-element.ts | 4 +- 6 files changed, 65 insertions(+), 76 deletions(-) delete mode 100644 app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts delete mode 100644 app/javascript/packages/webauthn/is-public-key-credential-supported.ts create mode 100644 app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.spec.ts create mode 100644 app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts diff --git a/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts b/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts deleted file mode 100644 index c1a983e7b80..00000000000 --- a/app/javascript/packages/webauthn/is-public-key-credential-supported.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useDefineProperty } from '@18f/identity-test-helpers'; -import isPublicKeyCredentialSupported from './is-public-key-credential-supported'; - -describe('isPublicKeyCredentialSupported', () => { - const defineProperty = useDefineProperty(); - - context('public key credential exists', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(true) }, - }); - }); - - it('resolves to true', () => { - expect(isPublicKeyCredentialSupported()).to.equal(true); - }); - - context('isUserVerifyingPlatformAuthenticatorAvailable is set to false', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(false) }, - }); - }); - - it('resolves to false', () => { - expect(isPublicKeyCredentialSupported()).to.equal(false); - }); - }); - }); - - context('public key credential does not exist ', () => { - beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: undefined, - }); - }); - - it('resolves to false', () => { - expect(isPublicKeyCredentialSupported()).to.equal(false); - }); - }); -}); diff --git a/app/javascript/packages/webauthn/is-public-key-credential-supported.ts b/app/javascript/packages/webauthn/is-public-key-credential-supported.ts deleted file mode 100644 index a1e301d108e..00000000000 --- a/app/javascript/packages/webauthn/is-public-key-credential-supported.ts +++ /dev/null @@ -1,7 +0,0 @@ -async function isPublicKeyCredentialSupported(): Promise { - const isUserVerifyingPlatformAuthenticatorAvailable = - await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); - return window.PublicKeyCredential && isUserVerifyingPlatformAuthenticatorAvailable; -} - -export default isPublicKeyCredentialSupported; diff --git a/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.spec.ts b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.spec.ts new file mode 100644 index 00000000000..9b940e1c3d2 --- /dev/null +++ b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.spec.ts @@ -0,0 +1,47 @@ +import { useDefineProperty } from '@18f/identity-test-helpers'; +import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; + +describe('isWebauthnPlatformAuthenticatorAvailable', () => { + const defineProperty = useDefineProperty(); + + context('browser does not support webauthn', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: undefined, + }); + }); + + it('resolves to false', async () => { + await expect(isWebauthnPlatformAuthenticatorAvailable()).to.eventually.equal(false); + }); + }); + + context('browser supports webauthn', () => { + context('device does not have platform authenticator available', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(false) }, + }); + }); + + it('resolves to false', async () => { + await expect(isWebauthnPlatformAuthenticatorAvailable()).to.eventually.equal(false); + }); + }); + + context('device has platform authenticator available', () => { + beforeEach(() => { + defineProperty(window, 'PublicKeyCredential', { + configurable: true, + value: { isUserVerifyingPlatformAuthenticatorAvailable: () => Promise.resolve(true) }, + }); + }); + + it('resolves to false', async () => { + await expect(isWebauthnPlatformAuthenticatorAvailable()).to.eventually.equal(true); + }); + }); + }); +}); diff --git a/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts new file mode 100644 index 00000000000..5cf986f2cd2 --- /dev/null +++ b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts @@ -0,0 +1,6 @@ +export type isWebauthnPlatformAvailable = () => Promise; + +const isWebauthnPlatformAuthenticatorAvailable: isWebauthnPlatformAvailable = async () => + !!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable()); + +export default isWebauthnPlatformAuthenticatorAvailable; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index 2c06afef16f..a065cd4a6a0 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,7 +1,7 @@ import sinon from 'sinon'; import quibble from 'quibble'; -import { useSandbox, useDefineProperty } from '@18f/identity-test-helpers'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; +import type { isWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; describe('WebauthnInputElement', () => { const isWebauthnPasskeySupported = sinon.stub< @@ -9,8 +9,10 @@ describe('WebauthnInputElement', () => { ReturnType >(); - const defineProperty = useDefineProperty(); - const sandbox = useSandbox(); + const isWebauthnPlatformAvailable = sinon.stub< + Parameters, + ReturnType + >(); before(async () => { quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); @@ -24,13 +26,8 @@ describe('WebauthnInputElement', () => { context('device does not support passkey', () => { context('unsupported passkey not shown', () => { beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { - isUserVerifyingPlatformAuthenticatorAvailable: sandbox.stub().resolves(true), - }, - }); isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.returns(Promise.resolve(false)); document.body.innerHTML = ``; }); @@ -44,6 +41,7 @@ describe('WebauthnInputElement', () => { context('unsupported passkey shown', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.returns(Promise.resolve(false)); document.body.innerHTML = ``; }); @@ -59,16 +57,12 @@ describe('WebauthnInputElement', () => { context('device supports passkey', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: undefined, - }); - + isWebauthnPlatformAvailable.returns(Promise.resolve(false)); isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); - it('becomes visible', () => { + it('stays hidden', () => { const element = document.querySelector('lg-webauthn-input')!; expect(element.hidden).to.be.true(); @@ -77,13 +71,7 @@ describe('WebauthnInputElement', () => { context('publickeycredential input is shown', () => { beforeEach(() => { - defineProperty(window, 'PublicKeyCredential', { - configurable: true, - value: { - isUserVerifyingPlatformAuthenticatorAvailable: sandbox.stub().resolves(true), - }, - }); - + isWebauthnPlatformAvailable.returns(Promise.resolve(true)); isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index bc74d0fdb74..954cac625c0 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,5 +1,5 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; -import isPublicKeyCredentialSupported from './is-public-key-credential-supported'; +import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { @@ -19,7 +19,7 @@ export class WebauthnInputElement extends HTMLElement { return; } - if (isWebauthnPasskeySupported() && (await isPublicKeyCredentialSupported())) { + if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; From 8322a9ea53477f04e4e84f8fa644ab07b3485402 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Tue, 21 Nov 2023 08:34:50 -0500 Subject: [PATCH 10/13] Fix webauthn setup linting --- app/javascript/packs/webauthn-setup.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index 7195ea4093c..614335d8d23 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -64,9 +64,8 @@ function webauthn() { (document.getElementById('client_data_json') as HTMLInputElement).value = result.clientDataJSON; if (result.authenticatorDataFlagsValue) { - ( - document.getElementById('authenticator_data_value') as HTMLInputElement - ).value = `${result.authenticatorDataFlagsValue}`; + (document.getElementById('authenticator_data_value') as HTMLInputElement).value = + `${result.authenticatorDataFlagsValue}`; } if (result.transports) { (document.getElementById('transports') as HTMLInputElement).value = From 4b8147ded13b72bc0c1a7abc62a9e4c8a1ca3916 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Tue, 21 Nov 2023 08:56:30 -0500 Subject: [PATCH 11/13] make sure to wait for async to pass before confirming test --- .../webauthn/webauthn-input-element.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index a065cd4a6a0..b03dd01c493 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -1,5 +1,6 @@ import sinon from 'sinon'; import quibble from 'quibble'; +import { waitFor } from '@testing-library/dom'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; import type { isWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; @@ -16,6 +17,7 @@ describe('WebauthnInputElement', () => { before(async () => { quibble('./is-webauthn-passkey-supported', isWebauthnPasskeySupported); + quibble('./is-webauthn-platform-authenticator-available', isWebauthnPlatformAvailable); await import('./webauthn-input-element'); }); @@ -27,7 +29,7 @@ describe('WebauthnInputElement', () => { context('unsupported passkey not shown', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); - isWebauthnPlatformAvailable.returns(Promise.resolve(false)); + isWebauthnPlatformAvailable.resolves(false); document.body.innerHTML = ``; }); @@ -41,7 +43,7 @@ describe('WebauthnInputElement', () => { context('unsupported passkey shown', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); - isWebauthnPlatformAvailable.returns(Promise.resolve(false)); + isWebauthnPlatformAvailable.resolves(false); document.body.innerHTML = ``; }); @@ -57,7 +59,7 @@ describe('WebauthnInputElement', () => { context('device supports passkey', () => { context('unsupported publickeycredential not shown', () => { beforeEach(() => { - isWebauthnPlatformAvailable.returns(Promise.resolve(false)); + isWebauthnPlatformAvailable.resolves(false); isWebauthnPasskeySupported.returns(true); document.body.innerHTML = ``; }); @@ -71,15 +73,15 @@ describe('WebauthnInputElement', () => { context('publickeycredential input is shown', () => { beforeEach(() => { - isWebauthnPlatformAvailable.returns(Promise.resolve(true)); isWebauthnPasskeySupported.returns(true); + isWebauthnPlatformAvailable.resolves(true); document.body.innerHTML = ``; }); - it('becomes visible', () => { + it('becomes visible', async () => { const element = document.querySelector('lg-webauthn-input')!; - expect(element.hidden).to.be.false(); + await waitFor(() => expect(element.hidden).to.be.false()); }); }); }); From f076ca84f84e1ae9073e63d294613703e829c63d Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Tue, 21 Nov 2023 10:25:26 -0500 Subject: [PATCH 12/13] add hidden spec fix --- spec/features/webauthn/hidden_spec.rb | 5 ++++- spec/support/features/webauthn_helper.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 28ba155b4bc..7567ee11224 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -2,6 +2,7 @@ RSpec.describe 'webauthn hide' do include JavascriptDriverHelper + include WebAuthnHelper describe 'security key' do let(:option_id) { 'two_factor_options_form_selection_webauthn' } @@ -58,9 +59,11 @@ expect(webauthn_option_hidden?).to eq(true) end - context 'with supported browser', driver: :headless_chrome_mobile do + context 'with supported browser and platform authenticator available', + driver: :headless_chrome_mobile do it 'displays the authenticator option' do sign_up_and_set_password + simulate_platform_authenticator_available expect(webauthn_option_hidden?).to eq(false) end diff --git a/spec/support/features/webauthn_helper.rb b/spec/support/features/webauthn_helper.rb index 963f6b2dda3..2800a436c60 100644 --- a/spec/support/features/webauthn_helper.rb +++ b/spec/support/features/webauthn_helper.rb @@ -127,6 +127,15 @@ def set_hidden_field(id, value) end end + def simulate_platform_authenticator_available + 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 + def protocol 'http://' end From d66a3378aabdd17827437deb8f79d8f770f5c8d1 Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Tue, 21 Nov 2023 11:00:58 -0500 Subject: [PATCH 13/13] fix capitalization --- .../is-webauthn-platform-authenticator-available.ts | 4 ++-- .../packages/webauthn/webauthn-input-element.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts index 5cf986f2cd2..673b59a5473 100644 --- a/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts +++ b/app/javascript/packages/webauthn/is-webauthn-platform-authenticator-available.ts @@ -1,6 +1,6 @@ -export type isWebauthnPlatformAvailable = () => Promise; +export type IsWebauthnPlatformAvailable = () => Promise; -const isWebauthnPlatformAuthenticatorAvailable: isWebauthnPlatformAvailable = async () => +const isWebauthnPlatformAuthenticatorAvailable: IsWebauthnPlatformAvailable = async () => !!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable()); export default isWebauthnPlatformAuthenticatorAvailable; diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index b03dd01c493..afd363cc143 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -2,7 +2,7 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { waitFor } from '@testing-library/dom'; import type { IsWebauthnPasskeySupported } from './is-webauthn-passkey-supported'; -import type { isWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; +import type { IsWebauthnPlatformAvailable } from './is-webauthn-platform-authenticator-available'; describe('WebauthnInputElement', () => { const isWebauthnPasskeySupported = sinon.stub< @@ -11,8 +11,8 @@ describe('WebauthnInputElement', () => { >(); const isWebauthnPlatformAvailable = sinon.stub< - Parameters, - ReturnType + Parameters, + ReturnType >(); before(async () => {