Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ webauthn_components-*.tar

# Temporary files, for example, from tests.
/tmp/

# Lexical's working directory
/.lexical/
44 changes: 39 additions & 5 deletions lib/webauthn_components/authentication_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,22 @@ 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.
- `@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.
- `@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.
Expand Down Expand Up @@ -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
Expand All @@ -81,8 +92,8 @@ defmodule WebauthnComponents.AuthenticationComponent do
disabled={@disabled}
data-skip-conditional-ui-check={if @skip_conditional_ui_check, do: "true"}
>
<span :if={@show_icon?} class="w-4 aspect-square opacity-70"><.icon_key /></span>
<span><%= @display_text %></span>
<span :if={@show_icon?} class="aspect-square w-4 opacity-70"><.icon_key /></span>
<span>{@display_text}</span>
</.button>

<input type="hidden" autocomplete="webauthn" />
Expand Down Expand Up @@ -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")

Expand All @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions lib/webauthn_components/registration_component.ex
Original file line number Diff line number Diff line change
@@ -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!
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be omitted.


> Registration = Sign Up

Expand Down Expand Up @@ -123,8 +123,8 @@ defmodule WebauthnComponents.RegistrationComponent do
title="Create a new account"
disabled={@disabled}
>
<span :if={@show_icon?} class="w-4 aspect-square opacity-70"><.icon_key /></span>
<span><%= @display_text %></span>
<span :if={@show_icon?} class="aspect-square w-4 opacity-70"><.icon_key /></span>
<span>{@display_text}</span>
Comment on lines +126 to +127
Copy link
Collaborator

Choose a reason for hiding this comment

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

These changes should be omitted since they're not related to the goals of the PR.

It may still be too early to convert to the new brackets syntax ({@display_text}) since there are possibly applications using this package with older versions of LiveView. I will consider making this change in a separate branch while updating dependencies.

</.button>
</span>
"""
Expand Down
31 changes: 31 additions & 0 deletions lib/webauthn_components/webauthn_credential.ex
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion priv/static/authentication_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -55,6 +61,9 @@ export const AuthenticationHook = {
timeout,
userVerification,
};

console.log(publicKey);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This console log should be removed.


const credential = await navigator.credentials.get({
publicKey,
signal: AbortControllerService.createNewAbortSignal(),
Expand Down
3 changes: 2 additions & 1 deletion priv/static/registration_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const RegistrationHook = {
const publicKey = {
attestation,
authenticatorSelection: {
authenticatorAttachment: "platform",
// authenticatorAttachment: "platform",
authenticatorAttachment: "all",
Comment on lines +51 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

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

It appears "all" is not a valid option here.

https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment

In fact, I could not find "all" anywhere in the spec, even going back to past drafts.

Ultimately, the plan is to make all of these parameters configurable from the Elixir code instead of hardcoding things in JS. That's out of scope for this branch, but worth noting.

residentKey: residentKey,
requireResidentKey: requireResidentKey,
},
Expand Down