WebAuthnLiveComponent - Passwordless Auth for LiveView Apps

Thanks for the work. I’m excited to start experimenting with webauthn.

In my trials I have an issue.

Registration and login works fine.

My home page and chat pages require login. I have the following in my router.ex, based on your demo repo:

live_session :default, on_mount: {ChatWeb.Hooks.User, :assign_user} do
    scope "/", ChatWeb do
      pipe_through(:browser)

      live "/sign-in", Live.SignIn
      live "/sign-out", Live.SignOut, :sign_out
    end
  end

  live_session :authenticated,
    on_mount: [{ChatWeb.Hooks.User, :assign_user}, {ChatWeb.Hooks.User, :require_user}] do
    scope "/", ChatWeb do
      pipe_through(:browser)

      live("/", HomeLive, :home)
      live("/topic/:topic_name", TopicLive, :topic)
    end

    scope "/user", ChatWeb do
      pipe_through :browser

      live "/profile", Live.UserProfile
    end

Everything works fine - registration, login, etc.

However, HomeLive and TopicLive both require user info to be present. They work after logging in or registering while navigating the app. However, if I refresh the page on either then they error out with current_user being nil in the socket.

I have worked around the problem by having, on both:

  1. a mount function that pattern matches for the existence of current_user e.g. %{assigns: %{current_user: %User{} = current_user}} = socket)
  2. a fallback mount function that returns {:ok, socket} and doesn’t do any assigns
  3. A render function that pattern matches for username, e.g. %{username: _} = assigns
  4. A fallback render function that renders a blank page.

With these in place, things work. I believe there is a blank render with no user info, then another render with the user info.

Is this expected behaviour, or am I doing something incorrect?

1 Like

Hey @mward-sudo! Thank you for the feedback.

Getting the session hooks right is a challenge since a refresh will load the page before the socket is connected. In the demo app, I can register, redirect to the login page, save changes, and refresh the page without error.

I think the key is to use assign_new/3 to ensure @current_user exists in the session hooks (example). If you’re already doing this, I suspect there is a pattern match somewhere which expects a %User{} struct, and may need to handle the initial nil value.

For what it’s worth, I’ve pulled back on pattern matches in LiveView code, especially matching on types of data. There are exceptions when the code is simple and compact. I started doing this to keep line length down since I typically fold LOC that I’m not working on, and it’s helped avoid some issues like this. Instead, I’ve started to prefer this approach where an assign may exist and the value may be nil or a struct.

Hope that helps.

1 Like

Thanks for the reply. I’ll take a detailed look at your suggestion and will let you know what I find.

1 Like

v0.4.0 Released

The latest release of WebauthnComponents adds a new required WebauthnUser struct during registration. This allows the parent LiveView to set the id, name, and display name used by the WebAuthn API.

For end users, the experience is greatly improved. Previously, the RegistrationComponent would display the app name (or whatever was passed as an @app assign. If multiple accounts were registered to the same device for an origin, each credential would have the same name, making it impossible to know which one should be selected.

Now, the parent LiveView must pass the new %WebauthnComponents.WebauthnUser{} struct to the RegistrationComponent before registering a user. You may choose to identify users by email, username, or whatever makes sense for the application.

Registration

As a result of this new requirement, multiple accounts registered on the same device will have clearer names displayed. During authentication, users will more easily be able to select the correct credential. If a user wants to remove credentials from their preferred manager, it will also be more straightforward to do so thanks to the updated naming requirement.

Authentication

PRs

TODOs

  • Review/improve email validation in the demo app
  • Update the UserProfile LiveView in the demo app to handle email updates separately now that email is part of the User schema
  • Clarify that email is only required for registration in the demo app

As always, feedback is welcome!

5 Likes

Looks like 1Password would add support for passkeys in June. I did not found any estimates for Bitwarden.

Do anybody know if there is any browser or browser extension that have passkey support? I would like to test it on Linux.

Edit: I got such (translated) message on Chrome:

this device does not support the type of security key this website is asking for

1 Like

Apparently Chrome on Linux doesn’t support Passkeys directly. You’d need to register with another device, then use the QR code in Linux Chrome in order to authenticate that device.

2 Likes

Ehhh … yet another Linux phobia … :scream: They support or plan to support every platform except Linux. :no_entry_sign: What I really don’t understand is when they add support for Mac then Linux is much closer to it than Windows, but somehow magically everyone always have a problem with it. :magic_wand: I would really understand if they would not have a client on Linux or if they would not have ChromeOS and Android (which are in fact Linux), but this is some kind of bad joke. :sweat: btw. If they do not plan to work on Linux and there is no ChromeOS ever mentioned then they favor other operating systems over their own? :person_facepalming: I understand that :moneybag: changes a lot, but it’s also not so easy to answer with it for every such ridiculous decision. :exploding_head:

I really like the idea behind passkeys :key: , so I was waiting :alarm_clock: for at least a basic support for development tests even in some nightly browser releases, but when once again corporations shows this kind of attitude I see that it would not be a real authentication alternative in at least next years regardless of how stable this library is. :slightly_frowning_face:

1 Like

I suspect it has more to do with OS support, which is more complicated in Linux due to the variety of distros. As a web-only dev, I could be wrong on that point.

There’s a thread on the Gnome forum asking about this, fwiw.

v0.4.1

Dependencies have been updated in the latest release. Thank you Kenneth Kostrešević!

3 Likes

Yeah and that’s why their own ChromeOS is not even mentioned … This argument died years ago since advanced Linux users have no problem with for example unpacking deb packages and installing apps for a specific user or simply compile from source.

TLDR; Below is more descriptive answer about packages in Linux.

Do you know how hard is to create a package? I didn’t liked that in selected by user packages to install are dependencies required to compile many apps using asdf version manager. I decided to make a quick research checking how hard is to create a meta package. It was so simple that I have created few meta packages each for different asdf install and one meta package to install all of them. Now when I want to install software needed on any deb-based distribution I copy-paste my files and run just one command to install meta package.

Here is all I need to do:

# create a parent directory for meta packages
mkdir my-packages
# enter parent directory
cd my-packages
# create at once all directories each for one meta package
mkdir meta-a meta-b meta-c …
# in simple loop enter every directory, generate ns-control file and enter parent directory
for dir in `ls`; do cd $dir && equivs-control ns-control && cd ..; done
# once all packages are generated I open directory using my text editor
subl .

Example ns-control file:

$ cat /usr/local/asdf-meta/asdf-erlang/ns-control
# important for package manager copy-paste data for all ns-control files
# package belongs to development group
Section: devel
# mark package as not necessary for OS
Priority: optional
# declare which standard we use when building package
Standards-Version: 3.9.2

# other copy-pate data for all ns-control files i.e. package name and maintainer
Package: asdf-erlang
Maintainer: Tomasz Sulima <email>

# all our hard work goes here …
# the biggest thing we need to do is copy-paste packages from documentation
# and separate their names using comma (,) character
Depends: autoconf,build-essential,libgl1-mesa-dev,libglu1-mesa-dev,libncurses-dev,libncurses5-dev,libpng-dev,libxml2-utils,m4
Recommends: fop,libssh-dev,libwxgtk-webview3.0-gtk3-dev,libwxgtk3.0-gtk3-dev,openjdk-11-jdk,unixodbc-dev,xsltproc
# a simple word or two …
Description: meta package for Erlang dependencies installed using asdf version manager

# you can also add more information, but those are completely optional and not needed in our case

Without comments it’s 8 terribly hard to maintain lines. Only 5 of them were copy-pasted for all packages. That’s inhuman. :joy:

But those are not dev files, right?

Right, I forgot about another loop. Here it goes:

# just one word have changed: equivs-control -> equivs-build
for dir in `ls`; do cd $dir && equivs-build ns-control && cd ..; done

ok, so those deb files can be used in some custom repository like for example sublime-text or vivaldi have, but how hard is to add them locally and what about updating them?

That’s also simple:

# let's go back to parent directory of the one containing all meta packages
cd ..
# copy this directory where you like
sudo cp my-packages /usr/local
# instruct apt about your custom repository
# trusted=yes is required unless you add extra file with repo information
# which is not needed in our case
echo "deb [trusted=yes] file:/usr/local/my-packages ./" | sudo tee /etc/apt/sources.list.d/my-packages.list
# create a new script available in `PATH` environment variable
subl ~/.local/bin/update-my-packages
# finally mark script as executable
chmod +x ~/.local/bin/update-my-packages

Finally here is our update script:

$ cat ~/.local/bin/update-my-packages 
#! /bin/bash
# enter directory we have just copied
cd /usr/local/my-packages
# remove old generated packages
rm Packages.gz
# this line we already well known …
for package in `ls`; do cd $package && equivs-build ns-control && cd ..; done
# all you have to do is to generate a Packages.gz
dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz
# we can safely remove unnecessary buildinfo and changes files
# generated using equivs-build command
rm */*.buildinfo */*.changes

# check if the script is runned with root rights or using `sudo` command
if [[ "$EUID" = 0 ]]; then
  # just update
  apt-get update
else
  # make sure to ask for password on next sudo
  sudo -k

  # update with sudo
  if sudo true; then
    sudo apt-get update
  fi
fi

Now every time you change any ns-control file all you have to do is to call update-my-packages script and the updated deb is available to install using apt. All you have to remember is to update package version. :wink:

Both research and copy-paste took me “5 min”. Not sure, maybe it’s too much for corporations, but for me that is yet another way to save lots of time especially when filtering out all that build-essentials staff when reading a list of install by user packages. It changed dozens dependency entries in just one entry which remains me that on next installation I need to copy said meta packages.


But wait - I still didn’t said the best thing …Regarding the magic complexity … I have followed Ubuntu documentation on MX Linux which is Debian (stable) and not Ubuntu-based distribution. I guess that it would work without problem on all Debian-based distributions.

Now let’s take a look at package types used in most distributions:

  1. deb (Debian based) - binary
  2. ebuild (Gentoo based) - compiles from source
  3. pkg.tar.zst (Arch Linux based) - compiles from source
  4. rpm (Fedora/SUSE based) - binary

Except above there are 3 “portable” formats: AppImage, flatpak and snap.

Most of packages are created by community, some of them are compiling apps from source. The most noticeable problem when changing distribution is glibc version change. You need to recompile (or install binary compiled for your glibc version) all of the software you have copied (I did so when tested copying asdf directory).

Most of work you have to do if you want to support lots of distributions:

  1. Check distrowatch for most popular distributions
  2. Collect package type (like deb) and glibc version data
  3. Compile your app from source for each glibc version
  4. Package to deb, pkg.tar.zst and rpm for each glibc + package type combination
  5. Create ebuild for new version, if you did not added dependencies it’s basically copy-paste with changing one number
  6. Optionally also package compiled binaries with AppImage, flatpak and snap
  7. Upload all packages you have generated to your repositories

So almost whole process you can automate using CI. The other things left is to first create said repositories which you do only once and update package dependencies if needed.


If you want to know more simply create a new topic and feel free to ping me. You can also take a look how simple documentation is. For example in case of meta packages you can see:
https://help.ubuntu.com/community/Repositories/Personal

Thank you for spending the time to share your thoughts. :pray: This feels a bit off-topic in the context of this thread, so I’d ask that the discussion of Linux package management find a better home. That writeup would be a great blog post for anyone who’s interested. :wink:

4 Likes

Hey it’s been a couple months since the latest progress update and commit. Wondering if this awesome project still has wings! i would love to implement passwordless auth to my projects someday using your package.

2 Likes

I want to chime in with a +1 as well and generally give some support and encouragement :clap:t2:

And show gratitude :pray:t2:

:otter:

1 Like

Wondering if this awesome project still has wings!

:heart_decoration: Thank you @artokun & @scoop! :heart_decoration:

I suppose an update would be appropriate.

By all means, feel free to try out webauthn_components now in a non-production environment. Feedback on documentation, the API, and the demo app would be great! If you have some familiarity with WebAuthn, you could even open an issue or PR if you have ideas for improvements.

Update

The development pause is a result of a few challenges:

  • I had been waiting for Passkey support in 1Password (finally in beta).
  • I hadn’t been working on any projects using Passkeys.
  • There’s not enough time in the day.

Last weekend, I installed the 1Password beta and successfully added Passkeys for a few personal accounts to observe the user flow. It’s nice having Passkeys stored in a cross-platform virtual device, but not a hard requirement for supporting Passkeys in an application.

This weekend, I plan on adding Passkeys to a side project. As part of this process, I will be making improvements to the package as issues surface.

I just spent some time working on the Phoenix app generator (PR 5505), and have a better understanding of Mix.Task and Mix.Generator. This will help with building code generators for the various schemas and migrations. As a result, adoption should be much easier, especially for greenfield apps.

I’ll keep this thread updated as things change.

Btw, welcome to ElixirForum, @artokun! :partying_face:

8 Likes

Update

I have opened a draft PR which adds a task for generating the core modules required for WebauthnComponents.

This PR is focused on generating the following files:

  • User schema, migration, and context
  • UserKey schema, migration, and context
  • UserToken schema, migration, and context

Some opinions and best practices are codified in the generated modules:

  1. Ecto.ULID is used as the primary key for each of the schemas.
  • ULIDs are essentially sortable UUIDs, which prevent user enumeration without adding much complexity.
  1. Each context only interacts directly with one schema.
  • This makes the contexts easier to read & maintain compared to combining them into one Identity or Authentication context.

Scope

To limit scope, I am also constraining the generators to work best with Phoenix apps with no existing authentication. It may be possible to add WebauthnComponents to apps with basic authentication (a la phx.gen.auth), but doing so seems quite complex.

User email confirmation is also out of scope for now.

Call for Feedback

Before I wrap up this PR and move on to generators for LiveView modules, I want your feedback.

  • Do you see any typos or blatant errors?
  • If these files were generated in a new application, would they be a good starting point?
  • Do you like the general direction of the project’s generators?
  • Is there a better name for the task than wac.install?

I’ll take feedback in this thread, but I’d prefer comments on the PR, especially as comments on the files for anything wrong or confusing.

In the meantime, I will be adding test generators for the contexts so they can be deployed with confidence.

4 Likes

Update

:tada: Version 0.5.0 has been released :tada:

This release adds a wac.install mix task, which will generate schemas, contexts, and migrations for the following schemas:

  • %User{}
  • %UserKey{}
  • %UserToken{}

Each schema has a corresponding context with CRUD functions, where each function is has some documentation and a typespec - except a few token helpers from phx.gen.auth, which were overlooked.

My hope is that this further simplifies the process of implementing Passkeys in new LiveView apps. The generator is a bit opinionated about id types. To prevent user enumeration while maintaining sortability, Ecto.ULID is used as the :id type on the schemas.

Also of note, *.get/2 functions return ok/error tuples, which may be a departure from other context modules in your existing app.

Thanks to @jc00ke for providing feedback on the templates PR.

8 Likes

Update

:tada: Version 0.6.0 has been released :tada:

This release extends the new wac.install Mix Task to generate all the code required to implement Passkeys in a new LiveView application. Also, the TokenComponent has been removed in favor of cookie-based session storage.

The readme and other documentation has been updated, and setup should now be a much more streamlined process. Using wac.install, you no longer need to generate the LiveView or other code to get up and running. See the wac.install docs for available options.

Thanks to @oullette @sax @mward-sudo and everyone who’s provided feedback on this project.

End User Demo

State of Passkeys

Passkey support has been picking up steam, and I’m looking forward to Windows adding support for credential management across devices. Recently, Passkey support graduated out of beta for 1Password, my preferred credential manager. MacOS and Android users can use their platform-specific credential managers as well.

Next Steps

My next planned improvement is to add Telemetry events to the components and remove all calls to Logger, with the goal of improving observability.

I would also like to document the process of implementing WebauthnComponents in existing applications, but this is a daunting task. Providing step-by-step migration from basic auth or 0auth seems feasible, though guides for migrating from phx.gen.auth, Pow, and various packages may put too much burden on maintenance. If you have thoughts or would like to contribute, feel free to ping me here or in an issue.

11 Likes

Dashlane also has passkeys support these days.

2 Likes

Update

:tada: Version 0.7.0 has been released, with one new feature and a bug fix.

  1. Now, WebauthnComponents supports Passkey autofill, which streamlines the authentication process for existing users.
  2. Migrations are now generated in a deterministic order, fixing a bug where the users could be generated after its dependent migrations.

Passkey Autofill

With the introduction of autofill, users will be presented with a Passkey prompt automatically after the authentication component is mounted. Previously, users would need to click the “authenticate” button first, often thinking the email field was also required. With autofill, users have a more seamless authentication experience. :surfing_woman:t4:

If a user has multiple accounts on an application, they will still be prompted to select the desired account.

Order of Migrations

Most of the templates and generators I created can be generated out of order, but migrations are an exception. Because migration files are generated with a timestamp and users must be created first, it’s important to create them in the correct order.

The migrations generator was using a plain Elixir map for its template files. Elixir maps are unordered, so the order of the keys in code are not necessarily the order they will be processed in Enum functions or for comprehensions. This is an oversight that catches me more often than I’d like to admit. :see_no_evil:

The solution was to convert the template map into a Keyword list, which maintains the order of its keys (fix commit). Now, we can be sure that the users table is created before the user_keys and user_tokens tables which depend on it. :dizzy:

Credits

Shout out to Daniel Pinheiro for introducing the autofill feature and to David Parry for reporting the migrations issue.

10 Likes

Hi Owen, thanks for the great library.
I just completed adding it to my existing Phoenix application and it works like a charm.
I did a little writeup on my blog to explain the steps I took.

10 Likes