Code Monkey home page Code Monkey logo

webauthn_components's Introduction

WebauthnComponents

Passkey authentication for Phoenix LiveView applications.

Project Status

  • This package is a work in progress, and it is in early beta status.
  • Please do not use WebauthnComponents in a production environment until it has completed beta testing.
  • Feel free to experiment with this package and contribute feedback in Elixir Forum.

Roadmap

View the planned work for this repo in the public WebauthnComponents v1 project on GitHub.

Quick Start

WebauthnComponents includes a Mix Task for scaffolding the files needed to implement Passkey support.

  1. Add the dependency in mix.exs ({:webauthn_components, "~> 0.6"}).
  2. Run mix deps.get to install WebauthnComponents.
  3. Run mix wac.install to scaffold the files.
  4. Run mix ecto.migrate to create the users, user_keys, and user_tokens tables.
  5. Review the code generated by wac.install.

Installation Details

๐Ÿ‘‰ The wac.install task is currently designed for Phoenix apps with no existing authentication.

If your application has existing authentication, you may manually add or modify the modules required by WebauthnComponents.

Create a new Phoenix Project

mix phx.new my_app
cd my_app

Add the Dependency

# my_app/mix.exs
def deps do
  [
    {:webauthn_components, "~> 0.5"}
  ]
end

Generate and Modify Code

WebauthnComponents comes with a wac.install Mix Task to generate and modify modules required to support Passkeys in a LiveView application.

๐Ÿ’พ ๐Ÿ’พ ๐Ÿ’พ
Schemas Migrations Context
GenServer Tests Fixtures
Components Controllers LiveView
Session Hooks Javascript Hooks Router

From the project directory, run mix wac.install to generate the required code:

mix wac.install
==> webauthn_components
Compiling 2 files (.ex)
Generated webauthn_components app
==> my_app
* creating lib/my_app/identity.ex
* creating lib/my_app/user_token_cleaner.ex
* creating lib/my_app/identity/user.ex
* creating lib/my_app/identity/user_key.ex
* creating lib/my_app/identity/user_token.ex
* creating priv/repo/migrations/20230917174312_users.exs
* creating priv/repo/migrations/20230917174313_user_keys.exs
* creating priv/repo/migrations/20230917174314_user_tokens.exs
* creating test/my_app/identity_test.exs
* creating test/my_app_web/live/authentication_live_test.exs
* creating test/support/identity_fixtures.ex
* creating lib/my_app_web/controllers/session.ex
* creating lib/my_app_web/live/authentication_live.ex
* creating lib/my_app_web/live/authentication_live.html.heex
* creating lib/my_app_web/session_hooks/assign_user.ex
* creating lib/my_app_web/session_hooks/require_user.ex
* creating lib/my_app_web/components/navigation_components.ex
* creating lib/my_app_web/components/navigation/navbar.html.heex
* creating lib/my_app_web/components/navigation/nav_link.html.heex
* updating assets/js/app.js
* updating lib/my_app_web/router.ex

โœ… Successfully scaffolded WebauthnComponents for MyApp

๐Ÿ“š Resources

- Repo: https://github.com/liveshowy/webauthn_components
- Hex:  https://hex.pm/packages/webauthn_components
- Docs: https://hexdocs.pm/webauthn_components/readme.html

Webauthn Flows

Documentation and illustrations for WebauthnComponent flows can be found in Webauthn Flows.

WebAuthn and Passkeys

What is the difference between "WebAuthn" and "Passkeys"? The first is an API available in modern browsers, and the second is an implementation where public key credentials created by the WebAuthn API can be saved to the client device and/or a cloud provider.

The following quotes were chosen for their brevity and clarity.

From MDN:

The Web Authentication API is an extension of the Credential Management API that enables strong authentication with public key cryptography, enabling passwordless authentication and/or secure second-factor authentication without SMS texts.

https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API

From Google:

A passkey is a FIDO login credential, tied to an origin (website or application) and a physical device. Passkeys allow users to authenticate without having to enter a username, password, or provide any additional authentication factor. This technology aims to replace passwords as the primary authentication mechanism.

https://developers.google.com/identity/fido

* While a user may technically be registered without a username or email, this is often a practical requirement for both a business and its users. By requiring an email, WebauthnComponents ensures each registered Passkey can be identified by the user when reviewing their collection of credentials. Email is often a required identifier for a business, especially when it needs to communicate with its users outside of its applications.

Benefits

There are many benefits to users and application maintainers when passwords are decommissioned.

  • Eliminates password reuse by users.
  • Mitigates credential stuffing attacks by hackers.
  • Eliminates phishing attacks by hackers.

For users on a device with Passkey support, WebAuthn credentials may be stored in the cloud. This allows the user to authenticate from other cloud-connected devices without registering each device individually.

Cross-Device Authentication

Passkeys may be synchronized across mutliple devices connected to a user's cloud account, where credentials are managed.

When a user attempts to authenticate on a device where their Passkey is not synchronized, they may scan a QR code to use a Passkey stored on the registered device. The browser will detect that no Passkey is registered for the site and render the QR code to be scanned by the device where the Passkey can be accessed.

Example

Imagine a user, Amal, registers a Passkey for example.com on their iPhone and it's stored in iCloud. When they attempt to sign into example.com on a non-Apple device or any browser which cannot access their OS keychain, they may choose to scan a QR code using their iPhone. Assuming the prompts on the iPhone are successful, the other device will be authenticated using the same web account which was initially registered on the iPhone.

While this example refers to Apple's Passkey implementation, the process on other platforms may vary. Cross-device credential managers like 1Password may provide a more seamless flow for users who are not constrained to one OS or browser.

Known Issues

While WebAuthn provides an API for improved authentication security, there are a few limitations to consider before adopting this component.

  • As of 2023, Passkeys are not universally supported.
  • Microsoft is testing Passkeys in Windows as of 6/29/2023 (article).
  • Cloud-synced credentials are only accessible to devices authenticated to the cloud account.
    • For example, a credential saved to iCloud Keychain will not be synced automatically to Android's credential manager.
    • 1Password has released Passkey support out of beta as of 9/2023.
    • Services like 1Password synchronize these credentials across platforms, but often require a subscription.
  • If a user registers or authenticates on a device without Passkey support, the generated key pair will only function on the device where it was registered. Each device must be registered in order to access an account.

Browser Support

The WebAuthn API has broad support across the most common modern browsers.

https://caniuse.com/?search=webauthn

Additional Resources

webauthn_components's People

Contributors

danielgpinheiro avatar ken-kost avatar kianmeng avatar mward-sudo avatar optikfluffel avatar peaceful-james avatar type1fool avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

webauthn_components's Issues

Improve Token Security

At the Denver Elixir meetup, one of the attendees (@RGENT) raised a concern about token security (video link).

In short, the token saved in sessionStorage may be accessed by 3rd party JS dependencies. Suggested mitigations include cookie storage and/or adding content security policy (CSP).

Because these component interact exclusively via WebSockets, cookies may not be an option here. CSP is likely the better solution, though other alternatives may exist.

Resources

Update Packages

Acceptance Criteria

  • Must run mix hex.outdated to list outdated deps
  • Must update Phoenix LiveView to 0.18.x
  • Must update Wax to latest version
  • May update other pkgs

[REQUEST] - Passkeys input autofill (Conditional UI)

Is your feature request related to a problem? Please describe.

I'm always frustrated when I need to input again and again my email when I need sign-in. Enabling Passkey Autofill with mediation: "conditional" and autocomplete="username webauthn" on email input, helps that.

Describe the solution you'd like

Implements Conditional UI to enable Passkey autofill

Additional context

Sign in with a passkey through form autofill
Add passkeys to the browser autofill
Explainer: WebAuthn Conditional UI

Notify

Support Resident Keys

Acceptance Criteria

  • Must add Passkey Component
  • Must add Passkey JS hook
  • Must allow user to register or authenticate without entering username/email/password

Resources

[BUG] - Value of form input for email is over-trusted

Description

The handle_info of :registration_successful trusts the assigned form value of the email. The email could be changed during the WebAuthn flow.

Steps To Reproduce

Steps to reproduce the behavior:

  1. Visit http://localhost:4000/sign-in
  2. Enter [email protected] into the email input.
  3. In console, run this JS:
function changeEmail() {
  var emailInput = document.getElementById("email")
  emailInput.value = "[email protected]";
  emailInput.dispatchEvent(new Event("input", {bubbles: true}))
}

setTimeout(changeEmail, 10000);
  1. Click "Sign Up".
  2. Wait at most 10 seconds for the email input to change (because of JS from step 3).
  3. Complete the sign up flow successfully.

Expected behavior

The difference in the form email and the email of the created passkey should cause user creation to fail.

Suggestion

Pass the email back as part of the :registration_successful payload and check that the form email matches exactly.

I do not think this is a realistic attack vector for any serious system. Email confirmation should be done after creating the passkey.

Fix RP Configuration

Description

The 0.6.3 update (diff) (PR) introduced a bug with the origin configuration.

It appears that by using the socket's host_uri, Wax's rp: :auto results in a mismatch, which prevents a valid credential from being used during authentication.

Image

Send the session token with the LiveView connect params, so that on_mount hooks and live_session can load/require users before any specific LiveView mount.

Is your feature request related to a problem? Please describe.

I've found this library to be a great start to adding passkeys to my application, but specific implementations make it challenging to add as-is. The token component is great, but when I have push navigation the current setup means that I have to check in all of my handle_params as well as all of my handle_event callbacks to know whether a user is already loaded, and every single live view I make needs to copy the same 3+ handle_info callbacks in order to work.

This might be more for the example repo than for the library itself, but if the hooks were broken apart a bit more it might be more clear how to use connect params to load a user in on_mount.

Describe the solution you'd like

  • Break out at least one utility function from the token hook, for getting and setting the session token.
  • (maybe) split the token component into two, one of which handles logout. This would mean that logged-in liveviews would only need to implement callbacks for token deletion, rather than all token callbacks.

Describe alternatives you've considered

I've instead used this library as a reference for writing my own components and hooks. If you find security issues or more complete integration code, I'll need to follow your fixes and apply them to my code.

Additional context

This is great work! It and your conference presentation were really helpful for me to add passkey support to my app.

Support Multiple Keys Per User

Often, a user may want to register additional keys after creating an account for backup or survivorship purposes. The Passkey package should provide an API for registering additional keys when an application wants to support this feature.

Acceptance Criteria

  • Must add SecondaryPasskey LiveComponent
  • Must pass user.id & user.displayName to handlePasskeyRegistration in passkey_hook.js
  • Must update demo app to illustrate component usage
  • TBD

Incorrect mix task for migrations

I believe the two mix commands in README.MD to generate the database structures should be using mix phx.gen.context rather than ecto.gen.migration

Add Generator for User, UserKey, UserToken, & UserProfile

With discoverable credentials, username and email data is not required up front. And, schemas for UserKey, UserToken, and UserProfile may differ in surprising ways from traditional applications.

So, custom generators may helpful for developers implementing Passkey support for the first time.

Acceptance Criteria

  • Must interpolate app and module names
  • Must generate Users context
  • Must generate User schema
  • Must generate UserKeys context
  • Must generate UserKey schema
  • Must generate UserTokens context
  • Must generate UserToken schema
  • Must generate context tests
  • Must document considerations and customizations

Bonus

  • If user schema already exists, prompt for appropriate action
    • For now, it may be wise to only allow the generator to run when there is no existing auth code.

Build Form Fields from Changeset

User Story

As a user of this component, I would like to provide my own changeset from my custom LiveView and see the form render fields based on the changeset's required fields.

Acceptance Criteria

  • Must define new changeset prop of type struct
  • Must define default changeset
  • Must list required fields and their types
  • Must generate form fields based on changeset fields and types

[REQUEST] - Separate LiveViews for "register" and "login", just like `phx.gen.auth`

The title says it all. I think the AuthenticationLive module should be split up.

The main reason is for ease of modification, then, I guess "separation of concerns".

I started implementing a 3rd flow for users to create a passkey for an existing account using email confirmation link, and quickly saw I would be using the RegistrationComponent for something that is not registration. That was problematic because the event-passing does not refer to the component ID or some other unique identifier. If they were in separate LiveViews, this would not be a problem.

On that note, I would also suggest renaming this to something more explicit like PasskeyCreationComponent, since it does not actually know or care about users being registered, only passkeys being created.

Any thoughts are welcome, I am recording "milestone" decisions that will be reflected in my future PR that will have split LIveViews, no user ID in the session and (hopefully) a "lost device" flow.

Document Implementation in Existing Apps

As of v0.6.0, WebauthnComponents can be used to easily scaffold Passkey authentication in new Phoenix apps. For existing applications with password-based authentication or 0Auth, more extensive documentation is needed.

Split Components

To support multiple keys for user accounts and MFA usage, and to separate concerns, the components need to be split up.

Acceptance Criteria

  • Must move registration to new RegistrationComponent
  • Must move authentication to new AuthenticationComponent
  • Must move token to new TokenComponent
  • Must move support to new SupportComponent
  • Must move registration to new registration_hook.js
  • Must move authentication to new authentication_hook.js
  • Must move token to new token_hook.js
  • Must move support checks to support_hook.js
  • Must remove PasskeyComponent

Context

Add Generator for MyApp.Hooks.User

Assigning and authenticating users with tokens in LiveView is a tedious process to implement. If possible, a generator for LiveView on_mount hooks would help developers get up to speed quickly, assuming requirements are met (see #15).

Acceptance Criteria

  • Must raise if User, UserKey, UserToken, or UserProfile schemas don't exist
  • Must generate :assign_user hook
  • Must generate :require_user hook
  • Must document considerations and customizability

Add Passkey Button

Acceptance Criteria

  • Must add new button type=button
  • Must be labeled "Sign in with Passkey"
  • May use key icon (mini variant)

Resources

  • Heroicons
    • Options: finger print, key*, cloud, face smile

[REQUEST] - mimic `renew_session` from `phx.gen.auth`

In the session.ex controller, supplant usages of clear_session with this function copied from phx.gen.auth:


  # This function renews the session ID and erases the whole
  # session to avoid fixation attacks. If there is any data
  # in the session you may want to preserve after log in/log out,
  # you must explicitly fetch the session data before clearing
  # and then immediately set it after clearing, for example:
  #
  #     defp renew_session(conn) do
  #       preferred_locale = get_session(conn, :preferred_locale)
  #
  #       conn
  #       |> configure_session(renew: true)
  #       |> clear_session()
  #       |> put_session(:preferred_locale, preferred_locale)
  #     end
  #
  defp renew_session(conn) do
    conn
    |> configure_session(renew: true)
    |> clear_session()
  end

[BUG] - migrations created non-deterministically

Description

Migrations are generated out of correct order, with the users table created after the migrations that depend on them.

Steps To Reproduce

Steps to reproduce the behavior:

  1. Run mix wac.install on a newly-generated Phoenix app, using Elixir 1.15.x.
  2. Generated migrations will (sometimes?) appear with the users table generated after the associated tables that require foreign key associations to that table. See listing:
priv/repo/migrations
โฏ
ls
.rw-r--r--  52 djp staff 2023-11-28 11:38 .formatter.exs
.rw-r--r-- 667 djp staff 2023-11-28 11:42 20231128004224_user_keys.exs
.rw-r--r-- 461 djp staff 2023-11-28 11:42 20231128004225_user_tokens.exs
.rw-r--r-- 300 djp staff 2023-11-28 11:42 20231128004226_users.exs
  1. Run mix ecto.migrate.
  2. See error:
mix ecto.migrate
Compiling 15 files (.ex)
Generated example app

11:42:39.934 [info] == Running 20231128004224 Example.Repo.Migrations.CreateUserKeys.change/0 forward

11:42:39.935 [info] create table user_keys
** (Postgrex.Error) ERROR 42P01 (undefined_table) relation "users" does not exist
    (ecto_sql 3.11.0) lib/ecto/adapters/sql.ex:1054: Ecto.Adapters.SQL.raise_sql_call_error/1
    (elixir 1.15.7) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2
    (ecto_sql 3.11.0) lib/ecto/adapters/sql.ex:1161: Ecto.Adapters.SQL.execute_ddl/4
    (ecto_sql 3.11.0) lib/ecto/migration/runner.ex:348: Ecto.Migration.Runner.log_and_execute_ddl/3
    (elixir 1.15.7) lib/enum.ex:1693: Enum."-map/2-lists^map/1-1-"/2
    (ecto_sql 3.11.0) lib/ecto/migration/runner.ex:311: Ecto.Migration.Runner.perform_operation/3
    (stdlib 3.17.2.4) timer.erl:166: :timer.tc/1
    (ecto_sql 3.11.0) lib/ecto/migration/runner.ex:25: Ecto.Migration.Runner.run/8

Expected behavior

users table should always be created first.

AdditionalCcontext

Elixir 1.15.7
Erlang 24.x

Notify

[BUG] - Does not work on Safari MacOS and iOS

Description

Running into issues where I can't use this on Safari. Both iOS and MacOS versions fail with different error messages.

On iOS I get a flash message when I go to the sign-in route that says "Passkeys not supported on this browser."

On MacOS I get errors in the console:

[debug] HANDLE EVENT "register" in MyPkAppWeb.AuthenticationLive
  Component: WebauthnComponents.RegistrationComponent
  Parameters: %{"value" => ""}
[debug] Replied in 542ยตs
[debug] HANDLE EVENT "error" in MyPkAppWeb.AuthenticationLive
  Component: WebauthnComponents.RegistrationComponent
  Parameters: %{"message" => "This request has been cancelled by the user.", "name" => "NotAllowedError", "stack" => ""}
[debug] Replied in 107ยตs
[warning] [unhandled_message: {MyPkAppWeb.AuthenticationLive, {:error, %{"message" => "This request has been cancelled by the user.", "name" => "NotAllowedError", "stack" => ""}}}]

To end on a positive note - both Firefox and Chrome on MacOS works as expected and across browsers! UX is ๐Ÿ‘Œ

Steps To Reproduce

Steps to reproduce the behavior:

  1. Instantiate a new Phoenix project
  2. Add deps
  3. Install deps and run ecto.create
  4. Run wac.install
  5. Start server
  6. Go to /sign-in
  7. Enter an email
  8. Click Sign Up button.

Expected behavior

I should be able to sign up and login like on Chrome and Firefox.

Desktop (please complete the following information)

  • OS: iOS, MacOS
  • Browser: Safari
  • Version: 17.3.1

Smartphone (please complete the following information)

  • Device: iPhone 15 Pro Max
  • OS: iOS
  • Browser: Safari
  • Version: 17

AdditionalCcontext

Add any other context about the problem here.

Notify

Store Token in Cookie

Instead of setting the token in JS SessionStorage, add a session controller which saves the token in a cookie.

This may eliminate the need for connected?(socket) checks.

Update the demo app to test & document usage.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.