Skip to content

Commit

Permalink
Check for user verifying platform authenticator (UVPA) (#75)
Browse files Browse the repository at this point in the history
* Use `residentKey` instead of `requireResidentKey`

* typo

* provide requireResidentKey as well, just in case

* Set require true only when r.key == required

* 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`

* fix compiler warning about missing parentheses

* Update templates/live_views/authentication_live.ex

Co-authored-by: Owen Bickford <[email protected]>

* write doc section for UVPA

* rename to make concise + default to false in HTML

* mix format

* make UVPA error message configurable

---------

Co-authored-by: Owen Bickford <[email protected]>
  • Loading branch information
peaceful-james and type1fool authored Jul 13, 2024
1 parent 9639617 commit 2bb677f
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 2 deletions.
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
22 changes: 21 additions & 1 deletion lib/webauthn_components/registration_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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.
- `@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
Expand All @@ -44,6 +46,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 +74,10 @@ 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(: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)
Expand Down Expand Up @@ -99,6 +117,8 @@ defmodule WebauthnComponents.RegistrationComponent do
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}
Expand All @@ -123,7 +143,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, {errorMessage: this.el.dataset.uvpa_error_message})
}

this.handleEvent("registration-challenge", (event) =>
this.handleRegistration(event, this)
);
},
async checkUserVerifyingPlatformAuthenticatorAvailable(context, {errorMessage}) {
if (!(await window.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable())) {
const error = new Error(errorMessage)
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

0 comments on commit 2bb677f

Please sign in to comment.