From e994b617d571881c6742e5e2de2719cf2828e5ec Mon Sep 17 00:00:00 2001 From: Dave Kimura Date: Wed, 14 Aug 2024 15:55:07 -0400 Subject: [PATCH] Passkey Login without email/password --- README.md | 5 ++-- .../javascripts/action_auth/application.js | 4 +++- .../stylesheets/action_auth/application.css | 6 +++++ .../sessions/passkeys_controller.rb | 24 +++++++++++++++++++ .../action_auth/magics/requests/new.html.erb | 9 +++++-- .../action_auth/registrations/new.html.erb | 3 +++ app/views/action_auth/sessions/new.html.erb | 11 ++++++--- .../sessions/passkeys/new.html.erb | 20 ++++++++++++++++ config/routes.rb | 5 ++++ lib/action_auth/configuration.rb | 5 ++++ 10 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 app/controllers/action_auth/sessions/passkeys_controller.rb create mode 100644 app/views/action_auth/sessions/passkeys/new.html.erb diff --git a/README.md b/README.md index db0bbc1..a935057 100755 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ ActionAuth.configure do |config| config.allow_user_deletion = true config.default_from_email = "from@example.com" config.magic_link_enabled = true + config.passkey_only = true # Allows sign in with only a passkey config.verify_email_on_sign_in = true config.webauthn_enabled = true config.webauthn_origin = "http://localhost:3000" # or "https://example.com" @@ -127,6 +128,8 @@ These are the planned features for ActionAuth. The ones that are checked off are ✅ - Passkeys/Hardware Security Keys +✅ - Passkeys sign in without email/password + ✅ - Magic Links ⏳ - OAuth with Google, Facebook, Github, Twitter, etc. @@ -141,8 +144,6 @@ These are the planned features for ActionAuth. The ones that are checked off are ⏳ - Account Impersonation - - ## Usage ### Routes diff --git a/app/assets/javascripts/action_auth/application.js b/app/assets/javascripts/action_auth/application.js index 7feea74..10133ce 100644 --- a/app/assets/javascripts/action_auth/application.js +++ b/app/assets/javascripts/action_auth/application.js @@ -43,7 +43,9 @@ const Credential = { get: function (credentialOptions) { const self = this; - const webauthnUrl = document.querySelector('meta[name="webauthn_auth_url"]').getAttribute("content"); + const webauthnUrlTag = document.querySelector('meta[name="passkey_auth_url"]') || + document.querySelector('meta[name="webauthn_auth_url"]'); + const webauthnUrl = webauthnUrlTag.getAttribute("content"); WebAuthnJSON.get({ "publicKey": credentialOptions }).then(function (credential) { self.callback(webauthnUrl, credential, "/"); }); diff --git a/app/assets/stylesheets/action_auth/application.css b/app/assets/stylesheets/action_auth/application.css index 8a0db82..514259c 100755 --- a/app/assets/stylesheets/action_auth/application.css +++ b/app/assets/stylesheets/action_auth/application.css @@ -36,6 +36,7 @@ body { margin-right: 5px; } } + .container-fluid { -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); @@ -78,6 +79,11 @@ input[type="password"] { margin-bottom: 1rem !important; } +.mx-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; +} + .btn { padding: 0.375rem 0.75rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/controllers/action_auth/sessions/passkeys_controller.rb b/app/controllers/action_auth/sessions/passkeys_controller.rb new file mode 100644 index 0000000..010e3aa --- /dev/null +++ b/app/controllers/action_auth/sessions/passkeys_controller.rb @@ -0,0 +1,24 @@ +module ActionAuth + module Sessions + class PasskeysController < ApplicationController + def new + get_options = WebAuthn::Credential.options_for_get + session[:current_challenge] = get_options.challenge + @options = get_options + end + + def create + webauthn_credential = WebAuthn::Credential.from_get(params) + credential = WebauthnCredential.find_by(external_id: webauthn_credential.id) + user = User.find_by(id: credential&.user_id) + if credential && user + session = user.sessions.create + cookies.signed.permanent[:session_token] = { value: session.id, httponly: true } + redirect_to main_app.root_path(format: :html), notice: "Signed in successfully" + else + redirect_to sign_in_path(format: :html), alert: "That passkey is incorrect" and return + end + end + end + end +end diff --git a/app/views/action_auth/magics/requests/new.html.erb b/app/views/action_auth/magics/requests/new.html.erb index b5b7509..71f69c2 100644 --- a/app/views/action_auth/magics/requests/new.html.erb +++ b/app/views/action_auth/magics/requests/new.html.erb @@ -1,4 +1,4 @@ -

Sign up

+

Request Magic Link

<%= form_with(url: magics_requests_path) do |form| %>
@@ -8,11 +8,16 @@
<%= form.submit "Request Magic Link", class: "btn btn-primary" %> + or + <%= link_to "Sign In", sign_in_path %> + <% if ActionAuth.configuration.passkey_only? %> + or + <%= link_to "Passkey", new_sessions_passkey_path %> + <% end %>
<% end %>
- <%= link_to "Sign In", sign_in_path %> | <%= link_to "Sign Up", sign_up_path %> | <%= link_to "Reset Password", new_identity_password_reset_path %> <% if ActionAuth.configuration.verify_email_on_sign_in %> diff --git a/app/views/action_auth/registrations/new.html.erb b/app/views/action_auth/registrations/new.html.erb index 6c7826f..a7d84fe 100755 --- a/app/views/action_auth/registrations/new.html.erb +++ b/app/views/action_auth/registrations/new.html.erb @@ -39,6 +39,9 @@ <% if ActionAuth.configuration.magic_link_enabled? %> <%= link_to "Magic Link", new_magics_requests_path %> | <% end %> + <% if ActionAuth.configuration.passkey_only? %> + <%= link_to "Passkey", new_sessions_passkey_path %> | + <% end %> <%= link_to "Reset Password", new_identity_password_reset_path %> <% if ActionAuth.configuration.verify_email_on_sign_in %> | <%= link_to "Verify Email", identity_email_verification_path %> diff --git a/app/views/action_auth/sessions/new.html.erb b/app/views/action_auth/sessions/new.html.erb index c369707..7fcece7 100755 --- a/app/views/action_auth/sessions/new.html.erb +++ b/app/views/action_auth/sessions/new.html.erb @@ -16,14 +16,19 @@
<%= form.submit "Sign in", class: "btn btn-primary" %> + <% if ActionAuth.configuration.magic_link_enabled? %> + or + <%= link_to "Magic Link", new_magics_requests_path %> + <% end %> + <% if ActionAuth.configuration.passkey_only? %> + or + <%= link_to "Passkey", new_sessions_passkey_path %> + <% end %>
<% end %>
<%= link_to "Sign Up", sign_up_path %> | - <% if ActionAuth.configuration.magic_link_enabled? %> - <%= link_to "Magic Link", new_magics_requests_path %> | - <% end %> <%= link_to "Reset Password", new_identity_password_reset_path %> <% if ActionAuth.configuration.verify_email_on_sign_in %> | <%= link_to "Verify Email", identity_email_verification_path %> diff --git a/app/views/action_auth/sessions/passkeys/new.html.erb b/app/views/action_auth/sessions/passkeys/new.html.erb new file mode 100644 index 0000000..f566763 --- /dev/null +++ b/app/views/action_auth/sessions/passkeys/new.html.erb @@ -0,0 +1,20 @@ +

Use a passkey to sign in

+<%= tag :meta, name: :passkey_auth_url, content: action_auth.sessions_passkeys_url %> + +<%= content_tag :div, + id: "webauthn_credential_form", + data: { + controller: "credential-authenticator", + "credential-authenticator-options-value": @options + }, + class: "action-auth--text-center" do %> + +
+ Insert a USB key, if necessary, and tap it. + An account with a matching passkey is required. +
+<% end %> + +<%= content_for :cancel_path do %> + <%= link_to "Cancel", action_auth.sign_in_path %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index e6dc9b0..fccb690 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,11 @@ resource :password_reset, only: [:new, :edit, :create, :update] end resource :password, only: [:edit, :update] + namespace :sessions do + if ActionAuth.configuration.webauthn_enabled? && ActionAuth.configuration.passkey_only? + resources :passkeys, only: [:new, :create] + end + end resources :sessions, only: [:index, :show, :destroy] if ActionAuth.configuration.allow_user_deletion? diff --git a/lib/action_auth/configuration.rb b/lib/action_auth/configuration.rb index 8df28aa..ee34043 100644 --- a/lib/action_auth/configuration.rb +++ b/lib/action_auth/configuration.rb @@ -14,6 +14,7 @@ def initialize @allow_user_deletion = true @default_from_email = "from@example.com" @magic_link_enabled = true + @passkey_only = true @pwned_enabled = defined?(Pwned) @verify_email_on_sign_in = true @webauthn_enabled = defined?(WebAuthn) @@ -29,6 +30,10 @@ def magic_link_enabled? @magic_link_enabled == true end + def passkey_only? + webauthn_enabled? && @passkey_only == true + end + def webauthn_enabled? @webauthn_enabled.respond_to?(:call) ? @webauthn_enabled.call : @webauthn_enabled end