diff --git a/.gitignore b/.gitignore
index 466a398..56d3c63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ webauthn_components-*.tar
# Temporary files, for example, from tests.
/tmp/
+
+# Lexical's working directory
+/.lexical/
\ No newline at end of file
diff --git a/lib/webauthn_components/authentication_component.ex b/lib/webauthn_components/authentication_component.ex
index 2c7d539..ac54616 100644
--- a/lib/webauthn_components/authentication_component.ex
+++ b/lib/webauthn_components/authentication_component.ex
@@ -23,7 +23,7 @@ defmodule WebauthnComponents.AuthenticationComponent do
## Assigns
- - `@challenge`: (Internal) A `Wax.Challenge` struct created by the component, used to request an existing credential in the client.
+ - `@challenge` (Internal) A `Wax.Challenge` struct created by the component, used to request an existing credential in the client.
- `@display_text` (Optional) The text displayed inside the button. Defaults to "Sign In".
- `@show_icon?` (Optional) Controls visibility of the key icon. Defaults to `true`.
- `@class` (Optional) CSS classes for overriding the default button style.
@@ -31,6 +31,14 @@ defmodule WebauthnComponents.AuthenticationComponent do
- `@id` (Optional) An HTML element ID.
- `@skip_conditional_ui_check` (Optional) Set to `true` to skip the conditional UI check for Passkey autofill. Defaults to `false`.
+ If the authenticator is not supporting resident keys you have to provide a list of `%WebauthnCredentials{}` either by using:
+ - `@user` (Optional) A user identifier like e-mail or user name as a string
+ - `@retrieve_credentials_function` (Optional) A function of the type `@spec retrieve_credentials_for(binary) :: [%WebauthnCredential{}]` which is supposed to get the IDs and the public keys for a given user
+
+ or
+
+ - `@allow_credentials` (Optional) A list of `%WebauthnCredentials{}`
+
## Events
- `"authenticate"`: Triggered when a user clicks the `authenticate` button.
@@ -63,6 +71,9 @@ defmodule WebauthnComponents.AuthenticationComponent do
|> assign_new(:display_text, fn -> "Sign In" end)
|> assign_new(:show_icon?, fn -> true end)
|> assign_new(:relying_party, fn -> nil end)
+ |> assign_new(:allow_credentials, fn -> [] end)
+ |> assign_new(:user, fn -> nil end)
+ |> assign_new(:retrieve_credentials_function, fn -> nil end)
|> assign_new(:skip_conditional_ui_check, fn -> false end)
}
end
@@ -81,8 +92,8 @@ defmodule WebauthnComponents.AuthenticationComponent do
disabled={@disabled}
data-skip-conditional-ui-check={if @skip_conditional_ui_check, do: "true"}
>
- <.icon_key />
- <%= @display_text %>
+ <.icon_key />
+ {@display_text}
@@ -133,7 +144,13 @@ defmodule WebauthnComponents.AuthenticationComponent do
def handle_event("authenticate", params, socket) do
%{assigns: assigns, endpoint: endpoint} = socket
- %{id: id} = assigns
+
+ %{
+ id: id,
+ allow_credentials: allow_credentials,
+ retrieve_credentials_function: retrieve_credentials_function,
+ user: user
+ } = assigns
supports_passkey_autofill = Map.has_key?(params, "supports_passkey_autofill")
@@ -142,18 +159,35 @@ defmodule WebauthnComponents.AuthenticationComponent do
do: "authentication-challenge-with-conditional-ui",
else: "authentication-challenge"
+ # If both, a function and a user are supplied, call that function to retrieve the credentials for
+ # the given user
+ # Otherwise: use the allow_credential list which might be preset instead
+ credentials =
+ if is_nil(retrieve_credentials_function) and is_nil(user) do
+ allow_credentials
+ else
+ retrieve_credentials_function.(user)
+ end
+
challenge =
Wax.new_authentication_challenge(
+ # WAX_ expects a list of maps, we have been using structs to do the xfer: So convert back
+ allow_credentials:
+ for credential <- credentials do
+ {credential.id, credential.public_key}
+ end,
origin: endpoint.url(),
rp_id: :auto,
user_verification: "preferred"
)
+ allow_credentials_ids = Enum.map(credentials, &Base.encode64(&1.id))
+
challenge_data = %{
challenge: Base.encode64(challenge.bytes, padding: false),
id: id,
rpId: challenge.rp_id,
- allowCredentials: challenge.allow_credentials,
+ allowCredentialsIDs: allow_credentials_ids,
userVerification: challenge.user_verification
}
diff --git a/lib/webauthn_components/registration_component.ex b/lib/webauthn_components/registration_component.ex
index 85956b2..d0e5650 100644
--- a/lib/webauthn_components/registration_component.ex
+++ b/lib/webauthn_components/registration_component.ex
@@ -1,6 +1,6 @@
defmodule WebauthnComponents.RegistrationComponent do
@moduledoc """
- A LiveComponent for registering a new Passkey via the WebAuthn API.
+ A LiveComponent for registering a new Passkey via the WebAuthn API!
> Registration = Sign Up
@@ -123,8 +123,8 @@ defmodule WebauthnComponents.RegistrationComponent do
title="Create a new account"
disabled={@disabled}
>
- <.icon_key />
- <%= @display_text %>
+ <.icon_key />
+ {@display_text}
"""
diff --git a/lib/webauthn_components/webauthn_credential.ex b/lib/webauthn_components/webauthn_credential.ex
new file mode 100644
index 0000000..21861ae
--- /dev/null
+++ b/lib/webauthn_components/webauthn_credential.ex
@@ -0,0 +1,31 @@
+defmodule WebauthnComponents.WebauthnCredential do
+ @moduledoc """
+ Struct representing a credential to be used by the WebAuthn API.
+ """
+
+ @enforce_keys [:id, :public_key]
+ defstruct [:id, :public_key]
+
+ @type t :: %__MODULE__{
+ id: binary(),
+ public_key: String.t()
+ }
+
+ defimpl Jason.Encoder, for: __MODULE__ do
+ def encode(struct, opts) do
+ map = Map.from_struct(struct)
+
+ encoded_public_key =
+ for {k, v} <- map[:public_key], into: %{} do
+ if is_binary(v) do
+ {k, Base.encode64(v)}
+ else
+ {k, v}
+ end
+ end
+
+ encoded_map = %{id: Base.encode64(map[:id]), public_key: encoded_public_key}
+ Jason.Encode.map(encoded_map, opts)
+ end
+ end
+end
diff --git a/priv/static/authentication_hook.js b/priv/static/authentication_hook.js
index a68eb1c..40be072 100644
--- a/priv/static/authentication_hook.js
+++ b/priv/static/authentication_hook.js
@@ -43,9 +43,15 @@ export const AuthenticationHook = {
async handlePasskeyAuthentication(event, context, mediation) {
try {
- const { challenge, timeout, rpId, allowCredentials, userVerification } =
+ const { challenge, timeout, rpId, allowCredentialsIDs, userVerification } =
event;
+ // allowCredentialsIDs is an array of already base64 encoded IDs
+ allowCredentials = new Array();
+ for (const id of allowCredentialsIDs) {
+ allowCredentials.push({ id: base64ToArray(id), type: 'public-key' });
+ };
+
const challengeArray = base64ToArray(challenge);
const publicKey = {
@@ -55,6 +61,9 @@ export const AuthenticationHook = {
timeout,
userVerification,
};
+
+ console.log(publicKey);
+
const credential = await navigator.credentials.get({
publicKey,
signal: AbortControllerService.createNewAbortSignal(),
diff --git a/priv/static/registration_hook.js b/priv/static/registration_hook.js
index e251e60..b06c18c 100644
--- a/priv/static/registration_hook.js
+++ b/priv/static/registration_hook.js
@@ -48,7 +48,8 @@ export const RegistrationHook = {
const publicKey = {
attestation,
authenticatorSelection: {
- authenticatorAttachment: "platform",
+ // authenticatorAttachment: "platform",
+ authenticatorAttachment: "all",
residentKey: residentKey,
requireResidentKey: requireResidentKey,
},