From 665302f85ffaf697adaabcea94ee4c8394fb8d31 Mon Sep 17 00:00:00 2001 From: James Gough Date: Sun, 30 Jun 2024 22:43:43 +0100 Subject: [PATCH 01/11] Use `residentKey` instead of `requireResidentKey` --- lib/webauthn_components/registration_component.ex | 14 ++++---------- priv/static/registration_hook.js | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 1149e971..af6f3929 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -17,7 +17,7 @@ defmodule WebauthnComponents.RegistrationComponent do - `@class` (Optional) CSS classes for overriding the default button style. - `@disabled` (Optional) Set to `true` when the `SupportHook` indicates WebAuthn is not supported or enabled by the browser. Defaults to `false`. - `@id` (Optional) An HTML element ID. - - `@require_resident_key` (Optional) Set to `false` to allow non-passkey credentials. Defaults to `true`. + - `@resident_key` (Optional) Set to `:preferred` or `:discouraged` to allow non-passkey credentials. Defaults to `:required`. ## Events @@ -59,7 +59,7 @@ defmodule WebauthnComponents.RegistrationComponent do |> assign_new(:class, fn -> "" end) |> assign_new(:webauthn_user, fn -> nil end) |> assign_new(:disabled, fn -> false end) - |> assign_new(:require_resident_key, fn -> true end) + |> assign_new(:resident_key, fn -> :required end) |> assign_new(:display_text, fn -> "Sign Up" end) |> assign_new(:show_icon?, fn -> true end) |> assign_new(:relying_party, fn -> nil end) @@ -112,13 +112,7 @@ defmodule WebauthnComponents.RegistrationComponent do def handle_event("register", _params, socket) do %{assigns: assigns, endpoint: endpoint} = socket - - %{ - app: app_name, - id: id, - require_resident_key: require_resident_key, - webauthn_user: webauthn_user - } = assigns + %{app: app_name, id: id, resident_key: resident_key, webauthn_user: webauthn_user} = assigns if not is_struct(webauthn_user, WebauthnUser) do raise "user must be a WebauthnComponents.WebauthnUser struct." @@ -139,7 +133,7 @@ defmodule WebauthnComponents.RegistrationComponent do challenge: Base.encode64(challenge.bytes, padding: false), excludeCredentials: [], id: id, - require_resident_key: require_resident_key, + resident_key: resident_key, rp: %{ id: challenge.rp_id, name: app_name diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js index 524f5bce..f289926d 100644 --- a/priv/static/registration_hook.js +++ b/priv/static/registration_hook.js @@ -16,7 +16,7 @@ export const RegistrationHook = { attestation, challenge, excludeCredentials, - requireResidentKey, + residentKey, rp, timeout, user, @@ -29,7 +29,7 @@ export const RegistrationHook = { attestation, authenticatorSelection: { authenticatorAttachment: "platform", - requireResidentKey: requireResidentKey, + residentKey: residentKey, }, challenge: challengeArray.buffer, excludeCredentials, From fcd2572b3ec3dd22520523abd42266a2a305fb7e Mon Sep 17 00:00:00 2001 From: James Gough Date: Mon, 1 Jul 2024 00:35:32 +0100 Subject: [PATCH 02/11] typo --- lib/webauthn_components/registration_component.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index af6f3929..93bbbc83 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -133,7 +133,7 @@ defmodule WebauthnComponents.RegistrationComponent do challenge: Base.encode64(challenge.bytes, padding: false), excludeCredentials: [], id: id, - resident_key: resident_key, + residentKey: resident_key, rp: %{ id: challenge.rp_id, name: app_name From fcc4c4dfec98897bc8b77f4adefe2dade8d27b65 Mon Sep 17 00:00:00 2001 From: James Gough Date: Mon, 1 Jul 2024 13:32:33 +0100 Subject: [PATCH 03/11] provide requireResidentKey as well, just in case --- lib/webauthn_components/registration_component.ex | 1 + priv/static/registration_hook.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 93bbbc83..617a76ad 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -134,6 +134,7 @@ defmodule WebauthnComponents.RegistrationComponent do excludeCredentials: [], id: id, residentKey: resident_key, + requireResidentKey: resident_key in [:required, :preferred], rp: %{ id: challenge.rp_id, name: app_name diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js index f289926d..722de43c 100644 --- a/priv/static/registration_hook.js +++ b/priv/static/registration_hook.js @@ -17,6 +17,7 @@ export const RegistrationHook = { challenge, excludeCredentials, residentKey, + requireResidentKey, rp, timeout, user, @@ -30,6 +31,7 @@ export const RegistrationHook = { authenticatorSelection: { authenticatorAttachment: "platform", residentKey: residentKey, + requireResidentKey: requireResidentKey, }, challenge: challengeArray.buffer, excludeCredentials, From 8825525043737b4e8077fa139c5cb47993d4c9fe Mon Sep 17 00:00:00 2001 From: James Gough Date: Tue, 2 Jul 2024 11:03:09 +0100 Subject: [PATCH 04/11] Set require true only when r.key == required --- lib/webauthn_components/registration_component.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 617a76ad..67d3d924 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -134,7 +134,7 @@ defmodule WebauthnComponents.RegistrationComponent do excludeCredentials: [], id: id, residentKey: resident_key, - requireResidentKey: resident_key in [:required, :preferred], + requireResidentKey: resident_key == :required, rp: %{ id: challenge.rp_id, name: app_name From ccb46b3f24753a72ef53a7be344248f543b520ea Mon Sep 17 00:00:00 2001 From: James Gough Date: Mon, 1 Jul 2024 00:14:02 +0100 Subject: [PATCH 05/11] Show error if client device can not store passkeys This can be bypassed by simply deleting the ``` check_user_verifying_platform_authenticator_available={true} ``` line, or setting it to `false` --- lib/webauthn_components/registration_component.ex | 3 +++ priv/static/registration_hook.js | 12 ++++++++++++ templates/live_views/authentication_live.ex | 7 +++++++ templates/live_views/authentication_live.html.heex | 1 + 4 files changed, 23 insertions(+) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 67d3d924..d810dbf1 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -18,6 +18,7 @@ defmodule WebauthnComponents.RegistrationComponent do - `@disabled` (Optional) Set to `true` when the `SupportHook` indicates WebAuthn is not supported or enabled by the browser. Defaults to `false`. - `@id` (Optional) An HTML element ID. - `@resident_key` (Optional) Set to `:preferred` or `:discouraged` to allow non-passkey credentials. Defaults to `:required`. + - `@check_user_verifying_platform_authenticator_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. ## Events @@ -60,6 +61,7 @@ defmodule WebauthnComponents.RegistrationComponent do |> assign_new(:webauthn_user, fn -> nil end) |> assign_new(:disabled, fn -> false end) |> assign_new(:resident_key, fn -> :required end) + |> assign_new(:check_user_verifying_platform_authenticator_available, fn -> false end) |> assign_new(:display_text, fn -> "Sign Up" end) |> assign_new(:show_icon?, fn -> true end) |> assign_new(:relying_party, fn -> nil end) @@ -99,6 +101,7 @@ defmodule WebauthnComponents.RegistrationComponent do phx-hook="RegistrationHook" phx-target={@myself} phx-click="register" + data-check_user_verifying_platform_authenticator_available={if @check_user_verifying_platform_authenticator_available, do: "true"} class={@class} title="Create a new account" disabled={@disabled} diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js index 722de43c..486369f8 100644 --- a/priv/static/registration_hook.js +++ b/priv/static/registration_hook.js @@ -5,10 +5,22 @@ export const RegistrationHook = { mounted() { console.info(`RegistrationHook mounted`); + if (this.el.dataset.check_user_verifying_platform_authenticator_available) { + this.checkUserVerifyingPlatformAuthenticatorAvailable(this) + } + this.handleEvent("registration-challenge", (event) => this.handleRegistration(event, this) ); }, + async checkUserVerifyingPlatformAuthenticatorAvailable(context) { + if (!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable())) { + const error = new Error("Registration unavailable. Your device does not support passkeys. Please install a passkey authenticator.") + error.name = "NoUserVerifyingPlatformAuthenticatorAvailable" + handleError(error, context); + throw error; + } + }, async handleRegistration(event, context) { try { diff --git a/templates/live_views/authentication_live.ex b/templates/live_views/authentication_live.ex index 63dfab57..38651ef6 100644 --- a/templates/live_views/authentication_live.ex +++ b/templates/live_views/authentication_live.ex @@ -165,6 +165,13 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do } end + def handle_info({:error, %{"message" => message, "name" => "NoUserVerifyingPlatformAuthenticatorAvailable"}}, socket) do + socket + |> assign(:token_form, nil) + |> put_toast(:error, message) + |> then(&{:noreply, &1}) + end + def handle_info(message, socket) do Logger.warning(unhandled_message: {__MODULE__, message}) {:noreply, socket} diff --git a/templates/live_views/authentication_live.html.heex b/templates/live_views/authentication_live.html.heex index 72bab18b..57df3ad4 100644 --- a/templates/live_views/authentication_live.html.heex +++ b/templates/live_views/authentication_live.html.heex @@ -22,6 +22,7 @@ module={RegistrationComponent} id="registration-component" app={<%= inspect @app_pascal_case %>} + check_user_verifying_platform_authenticator_available={true} class={[ "bg-gray-200 hover:bg-gray-300 hover:text-black rounded transition", "ring ring-transparent focus:ring-gray-400 focus:outline-none", From fbab1aa462178781547b1b80bce6cb277c718a36 Mon Sep 17 00:00:00 2001 From: James Gough Date: Mon, 1 Jul 2024 13:53:17 +0100 Subject: [PATCH 06/11] fix compiler warning about missing parentheses --- lib/webauthn_components/authentication_component.ex | 2 +- lib/webauthn_components/registration_component.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/webauthn_components/authentication_component.ex b/lib/webauthn_components/authentication_component.ex index 87a3407a..383cc8ff 100644 --- a/lib/webauthn_components/authentication_component.ex +++ b/lib/webauthn_components/authentication_component.ex @@ -139,7 +139,7 @@ defmodule WebauthnComponents.AuthenticationComponent do challenge = Wax.new_authentication_challenge( - origin: endpoint.url, + origin: endpoint.url(), rp_id: :auto, user_verification: "preferred" ) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index d810dbf1..ee96a6b0 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -126,7 +126,7 @@ defmodule WebauthnComponents.RegistrationComponent do challenge = Wax.new_registration_challenge( attestation: attestation, - origin: endpoint.url, + origin: endpoint.url(), rp_id: :auto, trusted_attestation_types: [:none, :basic] ) From dcded608bfdc2a895aff75e6cb37df412d442d8a Mon Sep 17 00:00:00 2001 From: Peaceful James <66864163+peaceful-james@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:10:37 +0100 Subject: [PATCH 07/11] Update templates/live_views/authentication_live.ex Co-authored-by: Owen Bickford --- templates/live_views/authentication_live.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/live_views/authentication_live.ex b/templates/live_views/authentication_live.ex index 38651ef6..1ff6a8a9 100644 --- a/templates/live_views/authentication_live.ex +++ b/templates/live_views/authentication_live.ex @@ -168,7 +168,7 @@ defmodule <%= inspect @web_pascal_case %>.AuthenticationLive do def handle_info({:error, %{"message" => message, "name" => "NoUserVerifyingPlatformAuthenticatorAvailable"}}, socket) do socket |> assign(:token_form, nil) - |> put_toast(:error, message) + |> put_flash(:error, message) |> then(&{:noreply, &1}) end From 2fc9820e897004a14544466d095af167d589dc0d Mon Sep 17 00:00:00 2001 From: James Gough Date: Tue, 2 Jul 2024 11:39:43 +0100 Subject: [PATCH 08/11] write doc section for UVPA --- lib/webauthn_components/registration_component.ex | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index ee96a6b0..4693bf9d 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -18,7 +18,7 @@ defmodule WebauthnComponents.RegistrationComponent do - `@disabled` (Optional) Set to `true` when the `SupportHook` indicates WebAuthn is not supported or enabled by the browser. Defaults to `false`. - `@id` (Optional) An HTML element ID. - `@resident_key` (Optional) Set to `:preferred` or `:discouraged` to allow non-passkey credentials. Defaults to `:required`. - - `@check_user_verifying_platform_authenticator_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. + - `@check_user_verifying_platform_authenticator_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. See the User Verifying Platform Authenticator section for more information. ## Events @@ -45,6 +45,18 @@ defmodule WebauthnComponents.RegistrationComponent do - `payload` contains the `message`, `name`, and `stack` returned by the browser upon timeout or other client-side errors. Errors should be displayed to the user via [`Phoenix.LiveView.put_flash/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#put_flash/3). However, some errors may be too technical or cryptic to be useful to users, so the parent LiveView may paraphrase the message for clarity. + + ## User Verifying Platform Authenticator + + The User Verifying Platform Authenticator (UVPA) is a special type of authenticator that requires user verification. + This is typically a biometric or PIN-based authenticator that is built into the platform, such as Touch ID or Windows Hello. + + When `@check_user_verifying_platform_authenticator_available` is set to `true`, the component will check if the user has a UVPA available before allowing registration. + If the user does not have a UVPA available, the component will disable the registration button and display a message indicating that the user must set up a UVPA before continuing. + + Example use case: + The first/primary credential on a sensitive account may be required to come from a platform authenticator. + Then, secondary credentials could be created from external devices. """ use Phoenix.LiveComponent import WebauthnComponents.IconComponents From e0d6e937c14f5b0956440d77affd961831952b7a Mon Sep 17 00:00:00 2001 From: James Gough Date: Tue, 2 Jul 2024 11:40:52 +0100 Subject: [PATCH 09/11] rename to make concise + default to false in HTML --- lib/webauthn_components/registration_component.ex | 10 ++++++---- priv/static/registration_hook.js | 2 +- templates/live_views/authentication_live.html.heex | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 4693bf9d..a642d2ae 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -18,7 +18,7 @@ defmodule WebauthnComponents.RegistrationComponent do - `@disabled` (Optional) Set to `true` when the `SupportHook` indicates WebAuthn is not supported or enabled by the browser. Defaults to `false`. - `@id` (Optional) An HTML element ID. - `@resident_key` (Optional) Set to `:preferred` or `:discouraged` to allow non-passkey credentials. Defaults to `:required`. - - `@check_user_verifying_platform_authenticator_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. See the User Verifying Platform Authenticator section for more information. + - `@check_uvpa_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. See the User Verifying Platform Authenticator section for more information. ## Events @@ -51,7 +51,7 @@ defmodule WebauthnComponents.RegistrationComponent do The User Verifying Platform Authenticator (UVPA) is a special type of authenticator that requires user verification. This is typically a biometric or PIN-based authenticator that is built into the platform, such as Touch ID or Windows Hello. - When `@check_user_verifying_platform_authenticator_available` is set to `true`, the component will check if the user has a UVPA available before allowing registration. + When `@check_uvpa_available` is set to `true`, the component will check if the user has a UVPA available before allowing registration. If the user does not have a UVPA available, the component will disable the registration button and display a message indicating that the user must set up a UVPA before continuing. Example use case: @@ -73,7 +73,7 @@ defmodule WebauthnComponents.RegistrationComponent do |> assign_new(:webauthn_user, fn -> nil end) |> assign_new(:disabled, fn -> false end) |> assign_new(:resident_key, fn -> :required end) - |> assign_new(:check_user_verifying_platform_authenticator_available, fn -> false end) + |> assign_new(:check_uvpa_available, fn -> false end) |> assign_new(:display_text, fn -> "Sign Up" end) |> assign_new(:show_icon?, fn -> true end) |> assign_new(:relying_party, fn -> nil end) @@ -113,7 +113,9 @@ defmodule WebauthnComponents.RegistrationComponent do phx-hook="RegistrationHook" phx-target={@myself} phx-click="register" - data-check_user_verifying_platform_authenticator_available={if @check_user_verifying_platform_authenticator_available, do: "true"} + data-check_uvpa_available={ + if @check_uvpa_available, do: "true" + } class={@class} title="Create a new account" disabled={@disabled} diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js index 486369f8..02f60327 100644 --- a/priv/static/registration_hook.js +++ b/priv/static/registration_hook.js @@ -5,7 +5,7 @@ export const RegistrationHook = { mounted() { console.info(`RegistrationHook mounted`); - if (this.el.dataset.check_user_verifying_platform_authenticator_available) { + if (this.el.dataset.check_uvpa_available) { this.checkUserVerifyingPlatformAuthenticatorAvailable(this) } diff --git a/templates/live_views/authentication_live.html.heex b/templates/live_views/authentication_live.html.heex index 57df3ad4..ae1e0a83 100644 --- a/templates/live_views/authentication_live.html.heex +++ b/templates/live_views/authentication_live.html.heex @@ -22,7 +22,7 @@ module={RegistrationComponent} id="registration-component" app={<%= inspect @app_pascal_case %>} - check_user_verifying_platform_authenticator_available={true} + check_uvpa_available={false} class={[ "bg-gray-200 hover:bg-gray-300 hover:text-black rounded transition", "ring ring-transparent focus:ring-gray-400 focus:outline-none", From 71aaa854fb2552d3d9d28b950740afd7f7718ccd Mon Sep 17 00:00:00 2001 From: James Gough Date: Tue, 2 Jul 2024 11:47:50 +0100 Subject: [PATCH 10/11] mix format --- lib/webauthn_components/registration_component.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index a642d2ae..702c8ce4 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -113,9 +113,7 @@ defmodule WebauthnComponents.RegistrationComponent do phx-hook="RegistrationHook" phx-target={@myself} phx-click="register" - data-check_uvpa_available={ - if @check_uvpa_available, do: "true" - } + data-check_uvpa_available={if @check_uvpa_available, do: "true"} class={@class} title="Create a new account" disabled={@disabled} From 62b1a2d4bbf807b1aff8bf4a18b264be4d499b5a Mon Sep 17 00:00:00 2001 From: James Gough Date: Thu, 11 Jul 2024 23:30:18 +0100 Subject: [PATCH 11/11] make UVPA error message configurable --- lib/webauthn_components/registration_component.ex | 5 +++++ priv/static/registration_hook.js | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex index 702c8ce4..85956b2b 100644 --- a/lib/webauthn_components/registration_component.ex +++ b/lib/webauthn_components/registration_component.ex @@ -19,6 +19,7 @@ defmodule WebauthnComponents.RegistrationComponent do - `@id` (Optional) An HTML element ID. - `@resident_key` (Optional) Set to `:preferred` or `:discouraged` to allow non-passkey credentials. Defaults to `:required`. - `@check_uvpa_available` (Optional) Set to `true` to check if the user has a platform authenticator available. Defaults to `false`. See the User Verifying Platform Authenticator section for more information. + - `@uvpa_error_message` (Optional) The message displayed when the user does not have a UVPA available. Defaults to "Registration unavailable. Your device does not support passkeys. Please install a passkey authenticator." ## Events @@ -74,6 +75,9 @@ defmodule WebauthnComponents.RegistrationComponent do |> assign_new(:disabled, fn -> false end) |> assign_new(:resident_key, fn -> :required end) |> assign_new(:check_uvpa_available, fn -> false end) + |> assign_new(:uvpa_error_message, fn -> + "Registration unavailable. Your device does not support passkeys. Please install a passkey authenticator." + end) |> assign_new(:display_text, fn -> "Sign Up" end) |> assign_new(:show_icon?, fn -> true end) |> assign_new(:relying_party, fn -> nil end) @@ -114,6 +118,7 @@ defmodule WebauthnComponents.RegistrationComponent do phx-target={@myself} phx-click="register" data-check_uvpa_available={if @check_uvpa_available, do: "true"} + data-uvpa_error_message={@uvpa_error_message} class={@class} title="Create a new account" disabled={@disabled} diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js index 02f60327..0cba98fa 100644 --- a/priv/static/registration_hook.js +++ b/priv/static/registration_hook.js @@ -6,16 +6,16 @@ export const RegistrationHook = { console.info(`RegistrationHook mounted`); if (this.el.dataset.check_uvpa_available) { - this.checkUserVerifyingPlatformAuthenticatorAvailable(this) + this.checkUserVerifyingPlatformAuthenticatorAvailable(this, {errorMessage: this.el.dataset.uvpa_error_message}) } this.handleEvent("registration-challenge", (event) => this.handleRegistration(event, this) ); }, - async checkUserVerifyingPlatformAuthenticatorAvailable(context) { + async checkUserVerifyingPlatformAuthenticatorAvailable(context, {errorMessage}) { if (!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable())) { - const error = new Error("Registration unavailable. Your device does not support passkeys. Please install a passkey authenticator.") + const error = new Error(errorMessage) error.name = "NoUserVerifyingPlatformAuthenticatorAvailable" handleError(error, context); throw error;