All of this code was stolen from various places. webauthn is a Ruby gem which has a Rails 6.0.6 demo: webauthn-rails-demo-app. webauthn-json is also being used on the browser side.
What I’ve attempted to do is make a demo that uses passkeys and Rails 7 as stock as possible. I’m using importmap-rails and tailwindcss-rails (although I don’t claim ANY ability to design web pages.
Also note that I am not good at writing tests.
There are two basic flows. One path is for a new unregistered user and the other path is for a previously registered user.
The database schema has a users
table which has a has_many
relationship to the credentials
table. The key piece of data is the
public_key
kept in the credentials
table.
An unregistered user will be directed to the registration/new
URL
where the registration ceremony will result in a new credential
being created for the user with their public_key
and other data. The
final step is a call to sign_in(user)
to store the new session that
has been created along the way.
A registered user will be directed to the session/new
URL which
retrieves the users public key from their associated credentials
and
performs the log in ceremony and calls sign_in(user)
assuming all
goes well.
home/index
is the root page for the application. HomeController
is a subclass of AuthenticatedController
which has a before_action
of enforce_current_user
. Any unauthenticated user is redirected
to session/new
. The session/new
page has a link to
registration/new
for unregistered users.
webauthn-rails-demo-app
had some errors. See Tangent to understand
Webauthn. Here are changes from that project:
RegistrationsController#create
- An existing user by the same name is found if it exists. This will allow a user to have more than one method of logging in. For example, they could have a touch-id as well as a Yubikey.
- The hash that is sent via JSON contains more than the credentials. There are no magic values in the Javascript such as the path to the callback function.
- The Options for Credential Creation is created by
WebAuthn::Credential#options_for_create
which returns aWebAuthn::PublicKeyCredential::CreationOptions
instance. The call tooptions_for_create
is an options hash that contains the following keys specified insnake_case
and are converted to camelBack via theto_json
method ofWebAuthn::PublicKeyCredential::CreationOptions
.rp
- The Relying Party is added in by the
options_for_create
method by what is specified inconfig/initializers/webauthn.rb
as documented in webauthn-ruby Configuration. user
- This is a hash with the following keys:
id
- This is a randomized sequence of bytes that should come
from
WebAuthn.generate_user_id
when the User model is created and preserved in the database. This is actually what the authentication ceremony looks at. Thename
anddisplayName
are merely for human consumption. name
- Generally the “User name”
display_name
- This is set equal to
name
if not explicitly supplied.
timeout
- An optional number specifying the number of milliseconds to wait.
attestation
- An optional attribute that is passed along unchanged.
authenticator_selection
- An optional hash that is passed
along unchanged. It too has
snake_case
keys which are converted to camelBack case. Authenticator Selection Criteria has these keys (given insnake_case
):authenticator_attachment
resident_key
require_resident_key
user_verification
exclude_credentials
- An optional list (Array) of PublicKeyCredentialDescriptors to exclude from the create processing.
exclude
- An optional more convenient method of creating
exclude_credentials
. Ifexclude_credentials
is not specified,exclude
can be specified as a list of strings and they are converted into tuples withid
being set to the string value andtype
being set to “public”. pub_key_cred_params
- An optional list of hashes with
type
key which should have a value of “public” and analg
key which should have a value of an algorithm id such as -7 for the ES256 algorithm. algs
- An optional and more convenient method for creating the
pub_key_cred_params
list. A list of algorithm names such as “ES256” is given. Each name is transformed into a hash containing a key oftype
with a value of “public” and the corresponding algorithm’s id.
- The original code had a field for
nickname
.nickname
appears four times in theWebAuthn
specification such as here but it is an algorithmic conversion not something the user inputs. Thusnickname
was removed from this implementation.
webauthn-json
- This implementation follows
webauthn-json
’s current recommendation of using browser ponyfill. - The original implementation used a Stimulus controller
feature_detection_controller to reveal a
div
if the browser could not support passkeys. The weakness is that if the feature detection completely fails, thediv
is not revealed leaving the user unaware that a problem has occurred.This implementation hides the
form
and shows the errordiv
until the feature detection completes successfully and happily at which point the errordiv
is hidden and theform
is revealed. (This has not been implemented yet.)
- This implementation follows