Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for user verifying platform authenticator (UVPA) #75

Merged
2 changes: 1 addition & 1 deletion lib/webauthn_components/authentication_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
17 changes: 16 additions & 1 deletion lib/webauthn_components/registration_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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_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

Expand All @@ -44,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_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:
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
Expand All @@ -60,6 +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_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)
Expand Down Expand Up @@ -99,6 +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"}
class={@class}
title="Create a new account"
disabled={@disabled}
Expand All @@ -123,7 +138,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]
)
Expand Down
12 changes: 12 additions & 0 deletions priv/static/registration_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ export const RegistrationHook = {
mounted() {
console.info(`RegistrationHook mounted`);

if (this.el.dataset.check_uvpa_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.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to give devs the ability to tailor this message to their user base. To do that, we may need to move the message into the LiveView. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK I did this with another data- html attribute.

error.name = "NoUserVerifyingPlatformAuthenticatorAvailable"
handleError(error, context);
throw error;
}
},

async handleRegistration(event, context) {
try {
Expand Down
7 changes: 7 additions & 0 deletions templates/live_views/authentication_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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_flash(:error, message)
|> then(&{:noreply, &1})
end

def handle_info(message, socket) do
Logger.warning(unhandled_message: {__MODULE__, message})
{:noreply, socket}
Expand Down
1 change: 1 addition & 0 deletions templates/live_views/authentication_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
module={RegistrationComponent}
id="registration-component"
app={<%= inspect @app_pascal_case %>}
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",
Expand Down
Loading