diff --git a/lib/wac_gen/app_html.ex b/lib/wac_gen/app_html.ex
index 6dff38c..adf596a 100644
--- a/lib/wac_gen/app_html.ex
+++ b/lib/wac_gen/app_html.ex
@@ -1,8 +1,6 @@
defmodule Wac.Gen.AppHtml do
@moduledoc false
- @header_regex ~r/\/mis
-
def update_app_html(assigns) do
web_snake_case = Keyword.fetch!(assigns, :web_snake_case)
file_path = Path.join(["lib", web_snake_case, "components", "layouts", "app.html.heex"])
@@ -10,11 +8,13 @@ defmodule Wac.Gen.AppHtml do
modified_file_contents =
file_path
|> File.read!()
- |> String.replace(@header_regex, navbar_component(assigns))
+ |> String.replace(header_regex(), navbar_component(assigns))
File.write!(file_path, modified_file_contents)
end
+ defp header_regex, do: ~r/\/mis
+
defp navbar_component(assigns) do
web_pascal_case = Keyword.fetch!(assigns, :web_pascal_case)
diff --git a/lib/wac_gen/javascript.ex b/lib/wac_gen/javascript.ex
index 28a3f53..eb86bcb 100644
--- a/lib/wac_gen/javascript.ex
+++ b/lib/wac_gen/javascript.ex
@@ -1,9 +1,6 @@
defmodule Wac.Gen.Javascript do
@moduledoc false
- @import_regex Regex.compile!("(import {\s?LiveSocket\s?} from \"phoenix_live_view\";?)")
- @socket_regex Regex.compile!("(params: {\s?_csrf_token: csrfToken\s?})")
-
@import_hooks """
import {
SupportHook,
@@ -22,9 +19,15 @@ defmodule Wac.Gen.Javascript do
updated_contents =
javascript_path
|> File.read!()
- |> String.replace(@import_regex, "\\1\n#{@import_hooks}")
- |> String.replace(@socket_regex, "\\1,\n#{@socket_hooks}")
+ |> String.replace(import_regex(), "\\1\n#{@import_hooks}")
+ |> String.replace(socket_regex(), "\\1,\n#{@socket_hooks}")
File.write!(javascript_path, updated_contents)
end
+
+ defp import_regex do
+ Regex.compile!("(import {\s?LiveSocket\s?} from \"phoenix_live_view\";?)")
+ end
+
+ defp socket_regex, do: Regex.compile!("(params: {\s?_csrf_token: csrfToken\s?})")
end
diff --git a/lib/webauthn_components/authentication_component.ex b/lib/webauthn_components/authentication_component.ex
index 2c7d539..96c0688 100644
--- a/lib/webauthn_components/authentication_component.ex
+++ b/lib/webauthn_components/authentication_component.ex
@@ -56,7 +56,7 @@ defmodule WebauthnComponents.AuthenticationComponent do
{
:ok,
socket
- |> assign(:challenge, fn -> nil end)
+ |> assign_new(:challenge, fn -> nil end)
|> assign_new(:id, fn -> "authentication-component" end)
|> assign_new(:class, fn -> "" end)
|> assign_new(:disabled, fn -> nil end)
diff --git a/lib/webauthn_components/icon_components.ex b/lib/webauthn_components/icon_components.ex
index ce9463e..48aed29 100644
--- a/lib/webauthn_components/icon_components.ex
+++ b/lib/webauthn_components/icon_components.ex
@@ -2,6 +2,15 @@ defmodule WebauthnComponents.IconComponents do
@moduledoc false
use Phoenix.Component
+ attr :type, :atom, required: true, values: [:key, :usb]
+
+ def icon(assigns) do
+ ~H"""
+ <.icon_key :if={@type == :key} />
+ <.icon_usb :if={@type == :usb} />
+ """
+ end
+
@doc false
def icon_key(assigns) do
~H"""
@@ -11,7 +20,7 @@ defmodule WebauthnComponents.IconComponents do
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
- class="w-full h-full"
+ class="w-full h-full min-w-4 min-h-4"
>
+
+
+ """
+ end
+
@doc false
def icon_info_circle(assigns) do
~H"""
diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex
index 85956b2..f65a974 100644
--- a/lib/webauthn_components/registration_component.ex
+++ b/lib/webauthn_components/registration_component.ex
@@ -12,11 +12,15 @@ defmodule WebauthnComponents.RegistrationComponent do
- `@user`: (**Required**) A `WebauthnComponents.WebauthnUser` struct.
- `@challenge`: (Internal) A `Wax.Challenge` struct created by the component, used to create a new credential request in the client.
- - `@display_text` (Optional) The text displayed inside the button. Defaults to "Sign Up".
+ - `@app`: (**Required**) The name of your application or service. This is displayed to the user during registration.
+ - `@authenticator_attachment` (Optional) The type of authenticator to use. Either `:platform` or `:cross_platform`. Defaults to `:platform`.
+ - `@display_text` (Optional) The text displayed inside the "platform" button. Defaults to "Sign Up" if authenticator attachment is `:platform`, or "Sign Up With Connected Device" if `:cross_platform`.
+ - `@icon_type` (Optional) The icon displayed inside the button. Either `:key` or `:usb`. Defaults to `:key` if authenticator attachment is `:platform`, or `:usb` if `:cross_platform`.
- `@show_icon?` (Optional) Controls visibility of the key icon. Defaults to `true`.
- `@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.
+ - `@timeout` (Optional) The timeout in milliseconds for the registration operation. Defaults to `60_000` (60 seconds).
- `@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."
@@ -61,7 +65,6 @@ defmodule WebauthnComponents.RegistrationComponent do
"""
use Phoenix.LiveComponent
import WebauthnComponents.IconComponents
- import WebauthnComponents.BaseComponents
alias WebauthnComponents.WebauthnUser
def mount(socket) do
@@ -69,8 +72,8 @@ defmodule WebauthnComponents.RegistrationComponent do
:ok,
socket
|> assign(:challenge, fn -> nil end)
- |> assign_new(:id, fn -> "registration-component" end)
|> assign_new(:class, fn -> "" end)
+ |> assign_new(:timeout, fn -> 60_000 end)
|> assign_new(:webauthn_user, fn -> nil end)
|> assign_new(:disabled, fn -> false end)
|> assign_new(:resident_key, fn -> :required end)
@@ -78,7 +81,6 @@ defmodule WebauthnComponents.RegistrationComponent do
|> 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)
}
@@ -98,11 +100,11 @@ defmodule WebauthnComponents.RegistrationComponent do
end
def update(assigns, socket) do
- {
- :ok,
- socket
- |> assign(assigns)
- }
+ socket
+ |> assign(assigns)
+ |> assign_new(:authenticator_attachment, fn -> :platform end)
+ |> assign_authenticator_attachment_dependant_assigns()
+ |> then(&{:ok, &1})
end
def render(assigns) do
@@ -111,28 +113,37 @@ defmodule WebauthnComponents.RegistrationComponent do
end
~H"""
-
- <.button
- id={@id}
- phx-hook="RegistrationHook"
- 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}
- >
- <.icon_key />
- <%= @display_text %>
-
-
+
"""
end
def handle_event("register", _params, socket) do
%{assigns: assigns, endpoint: endpoint} = socket
- %{app: app_name, id: id, resident_key: resident_key, webauthn_user: webauthn_user} = assigns
+
+ %{
+ app: app_name,
+ authenticator_attachment: authenticator_attachment,
+ id: id,
+ resident_key: resident_key,
+ webauthn_user: webauthn_user,
+ timeout: timeout
+ } = assigns
if not is_struct(webauthn_user, WebauthnUser) do
raise "user must be a WebauthnComponents.WebauthnUser struct."
@@ -148,19 +159,26 @@ defmodule WebauthnComponents.RegistrationComponent do
trusted_attestation_types: [:none, :basic]
)
+ authenticator_attachment_string =
+ case authenticator_attachment do
+ :platform -> "platform"
+ :cross_platform -> "cross-platform"
+ end
+
challenge_data = %{
- attestation: attestation,
- challenge: Base.encode64(challenge.bytes, padding: false),
- excludeCredentials: [],
- id: id,
- residentKey: resident_key,
- requireResidentKey: resident_key == :required,
- rp: %{
- id: challenge.rp_id,
- name: app_name
+ "attestation" => attestation,
+ "authenticatorAttachment" => authenticator_attachment_string,
+ "challenge" => Base.encode64(challenge.bytes, padding: false),
+ "excludeCredentials" => [],
+ "id" => id,
+ "residentKey" => resident_key,
+ "requireResidentKey" => resident_key == :required,
+ "rp" => %{
+ "id" => challenge.rp_id,
+ "name" => app_name
},
- timeout: 60_000,
- user: webauthn_user
+ "timeout" => timeout,
+ "user" => webauthn_user
}
{
@@ -208,4 +226,28 @@ defmodule WebauthnComponents.RegistrationComponent do
send(self(), {:invalid_event, event, payload})
{:noreply, socket}
end
+
+ defp assign_authenticator_attachment_dependant_assigns(socket) do
+ %{authenticator_attachment: authenticator_attachment} = socket.assigns
+
+ socket
+ |> assign_new(:id, fn -> default_id(authenticator_attachment) end)
+ |> assign_new(:display_text, fn -> default_display_text(authenticator_attachment) end)
+ |> assign_new(:icon_type, fn -> default_icon_type(authenticator_attachment) end)
+ end
+
+ defp default_id(authenticator_attachment) do
+ "registration-component-#{authenticator_attachment}"
+ end
+
+ defp default_display_text(:platform) do
+ "Sign Up With Passkey"
+ end
+
+ defp default_display_text(:cross_platform) do
+ "Sign Up With Connected Device"
+ end
+
+ defp default_icon_type(:platform), do: :key
+ defp default_icon_type(:cross_platform), do: :usb
end
diff --git a/priv/static/registration_hook.ts b/priv/static/registration_hook.ts
index 5ce8c7f..6c2ee0e 100644
--- a/priv/static/registration_hook.ts
+++ b/priv/static/registration_hook.ts
@@ -10,9 +10,11 @@ export const RegistrationHook = {
});
}
- this.handleEvent("registration-challenge", (event) =>
- this.handleRegistration(event, this)
- );
+ this.handleEvent("registration-challenge", (event) => {
+ if (event.id == this.el.id) {
+ this.handleRegistration(event, this);
+ }
+ });
},
async checkUserVerifyingPlatformAuthenticatorAvailable(
context,
@@ -32,6 +34,7 @@ export const RegistrationHook = {
try {
const {
attestation,
+ authenticatorAttachment,
challenge,
excludeCredentials,
residentKey,
@@ -47,7 +50,7 @@ export const RegistrationHook = {
const publicKey: PublicKeyCredentialCreationOptions = {
attestation,
authenticatorSelection: {
- authenticatorAttachment: "platform",
+ authenticatorAttachment: authenticatorAttachment,
residentKey: residentKey,
requireResidentKey: requireResidentKey,
},
diff --git a/templates/live_views/registration_live.ex b/templates/live_views/registration_live.ex
index f237d8d..ac6314c 100644
--- a/templates/live_views/registration_live.ex
+++ b/templates/live_views/registration_live.ex
@@ -52,7 +52,12 @@ defmodule <%= inspect @web_pascal_case %>.RegistrationLive do
|> Map.put(:name, email)
|> Map.put(:display_name, email)
- send_update(RegistrationComponent, id: "registration-component", webauthn_user: webauthn_user)
+ for authenticator_attachment <- [:platform, :cross_platform] do
+ send_update(RegistrationComponent,
+ id: "registration-component-#{authenticator_attachment}",
+ webauthn_user: webauthn_user
+ )
+ end
{
:noreply,
diff --git a/templates/live_views/registration_live.html.heex b/templates/live_views/registration_live.html.heex
index 9ba1f52..babdfc0 100644
--- a/templates/live_views/registration_live.html.heex
+++ b/templates/live_views/registration_live.html.heex
@@ -16,12 +16,31 @@
>
Create a new account:
- <.input type="email" field={form[:email]} label="Email" phx-debounce="250" autocomplete="username webauthn" />
+ <.input
+ type="email"
+ field={form[:email]}
+ label="Email"
+ phx-debounce="250"
+ autocomplete="username webauthn"
+ />
<.live_component
+ :for={
+ {authenticator_attachment, display_text} <- [
+ platform: dpgettext("registration", "buttons", "Register using a passkey"),
+ cross_platform:
+ dpgettext(
+ "registration",
+ "buttons",
+ "Register using a passkey with an external device (e.g. phone, security key, etc.)"
+ )
+ ]
+ }
+ authenticator_attachment={authenticator_attachment}
+ display_text={display_text}
+ id={"registration-component-#{authenticator_attachment}"}
disabled={@form.source.valid? == false}
module={RegistrationComponent}
- id="registration-component"
- app={<%= inspect @app_pascal_case %>}
+ app={inspect(@app_pascal_case)}
check_uvpa_available={false}
class={[
"bg-gray-200 hover:bg-gray-300 hover:text-black rounded transition",
@@ -31,7 +50,6 @@
]}
/>
-
<.link navigate={~p"/sign-in"} class="underline">Sign into an existing account
diff --git a/test/webauthn_components/registration_component_test.exs b/test/webauthn_components/registration_component_test.exs
index fb21a45..faf44f2 100644
--- a/test/webauthn_components/registration_component_test.exs
+++ b/test/webauthn_components/registration_component_test.exs
@@ -50,8 +50,9 @@ defmodule WebauthnComponents.RegistrationComponentTest do
assert clicked_element =~ "phx-click=\"register\""
assert_push_event(view, "registration-challenge", %{
- id: "registration-component",
- user: ^webauthn_user
+ "id" => "registration-component",
+ "user" => ^webauthn_user,
+ "authenticatorAttachment" => "platform"
})
end
end