has_secure_passkey 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/@github--webauthn-json.js +1 -0
- data/app/assets/javascripts/@rails--request.js +2 -0
- data/app/assets/javascripts/components/has_secure_passkey.js +3 -0
- data/app/assets/javascripts/components/web_authn.js +81 -0
- data/app/assets/javascripts/has_secure_passkey.js +406 -0
- data/app/assets/stylesheets/has_secure_passkey/application.css +15 -0
- data/app/controllers/has_secure_passkey/application_controller.rb +4 -0
- data/app/controllers/has_secure_passkey/challenges_controller.rb +6 -0
- data/app/helpers/has_secure_passkey/application_helper.rb +32 -0
- data/app/mailers/has_secure_passkey/application_mailer.rb +6 -0
- data/app/models/has_secure_passkey/application_record.rb +5 -0
- data/app/views/has_secure_passkey/challenges/create.turbo_stream.erb +5 -0
- data/app/views/layouts/has_secure_passkey/application.html.erb +17 -0
- data/config/importmap.rb +1 -0
- data/config/routes.rb +3 -0
- data/lib/generators/has_secure_passkey/passkeys/passkeys_generator.rb +59 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/channels/application_cable/connection.rb.tt +16 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/concerns/authentication.rb.tt +59 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/email_verifications_controller.rb.tt +7 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/people_controller.rb.tt +11 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/registrations_controller.rb.tt +20 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/sessions_controller.rb.tt +20 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/mailers/email_verification_mailer.rb.tt +7 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/models/current.rb.tt +4 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/models/passkey.rb.tt +30 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/models/person.rb.tt +9 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/models/session.rb.tt +7 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verification_mailer/verify.html.erb.tt +1 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verification_mailer/verify.text.erb.tt +1 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verifications/show.html.erb.tt +11 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/views/registrations/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/app/views/registrations/new.html.erb.tt +16 -0
- data/lib/generators/has_secure_passkey/passkeys/templates/test/mailers/previews/email_verification_mailer_preview.rb.tt +7 -0
- data/lib/has_secure_passkey/active_record_helpers.rb +48 -0
- data/lib/has_secure_passkey/add_passkey.rb +45 -0
- data/lib/has_secure_passkey/authenticate_by.rb +49 -0
- data/lib/has_secure_passkey/engine.rb +41 -0
- data/lib/has_secure_passkey/options_for_create.rb +49 -0
- data/lib/has_secure_passkey/options_for_get.rb +34 -0
- data/lib/has_secure_passkey/version.rb +3 -0
- data/lib/has_secure_passkey.rb +12 -0
- data/lib/tasks/has_secure_passkey_tasks.rake +5 -0
- metadata +113 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Has secure passkey</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<%= stylesheet_link_tag "has_secure_passkey/application", media: "all" %>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
|
14
|
+
<%= yield %>
|
15
|
+
|
16
|
+
</body>
|
17
|
+
</html>
|
data/config/importmap.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pin "has_secure_passkey", to: "has_secure_passkey.js"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class HasSecurePasskey::PasskeysGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def create_passkey_files
|
7
|
+
template "app/models/session.rb"
|
8
|
+
template "app/models/person.rb"
|
9
|
+
template "app/models/current.rb"
|
10
|
+
template "app/models/passkey.rb"
|
11
|
+
|
12
|
+
template "app/controllers/concerns/authentication.rb"
|
13
|
+
|
14
|
+
template "app/controllers/registrations_controller.rb"
|
15
|
+
template "app/views/registrations/create.turbo_stream.erb"
|
16
|
+
template "app/views/registrations/new.html.erb"
|
17
|
+
|
18
|
+
template "app/controllers/email_verifications_controller.rb"
|
19
|
+
template "app/views/email_verifications/show.html.erb"
|
20
|
+
|
21
|
+
template "app/controllers/people_controller.rb"
|
22
|
+
|
23
|
+
template "app/controllers/sessions_controller.rb"
|
24
|
+
|
25
|
+
template "app/channels/application_cable/connection.rb" if defined?(ActionCable::Engine)
|
26
|
+
|
27
|
+
template "app/mailers/email_verification_mailer.rb"
|
28
|
+
|
29
|
+
template "app/views/email_verification_mailer/verify.html.erb"
|
30
|
+
template "app/views/email_verification_mailer/verify.text.erb"
|
31
|
+
|
32
|
+
template "test/mailers/previews/email_verification_mailer_preview.rb"
|
33
|
+
|
34
|
+
append_to_file "app/javascript/application.js", "import \"has_secure_passkey\"\n"
|
35
|
+
|
36
|
+
insert_into_file "config/environments/development.rb", " config.x.url = \"http://localhost:3000\"\n", after: "Rails.application.configure do\n"
|
37
|
+
insert_into_file "config/environments/test.rb", " config.x.url = \"http://example.com\"\n", after: "Rails.application.configure do\n"
|
38
|
+
insert_into_file "config/environments/production.rb", " config.x.url = \"https://example.com\"\n", after: "Rails.application.configure do\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def configure_application_controller
|
42
|
+
inject_into_class "app/controllers/application_controller.rb", "ApplicationController", " include Authentication\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
def configure_passkey_routes
|
46
|
+
route "resource :sessions, only: :destroy"
|
47
|
+
route "resources :sessions, only: :create"
|
48
|
+
route "resources :people, only: :create"
|
49
|
+
route "resources :email_verifications, only: :show, param: :webauthn_message"
|
50
|
+
route "resource :registration, only: %i(new create)"
|
51
|
+
route "mount HasSecurePasskey::Engine => \"/has_secure_passkey\""
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_migrations
|
55
|
+
generate "migration", "CreatePeople", "email_address:string!:uniq webauthn_id:string!", "--force"
|
56
|
+
generate "migration", "CreateSessions", "person:references ip_address:string user_agent:string", "--force"
|
57
|
+
generate "migration", "CreatePasskeys", "authenticatable:references{polymorphic} external_id:string public_key:string sign_count:integer last_used_at:datetime name:string", "--force"
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ApplicationCable
|
2
|
+
class Connection < ActionCable::Connection::Base
|
3
|
+
identified_by :current_person
|
4
|
+
|
5
|
+
def connect
|
6
|
+
set_current_person || reject_unauthorized_connection
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def set_current_person
|
11
|
+
if session = Session.from_cookie(cookies)
|
12
|
+
self.current_person = session.person
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Authentication
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :require_authentication
|
6
|
+
helper_method :authenticated?
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def allow_unauthenticated_access(**options)
|
11
|
+
skip_before_action :require_authentication, **options
|
12
|
+
end
|
13
|
+
|
14
|
+
def prevent_authenticated_access(**options)
|
15
|
+
before_action :require_unauthenticated, **options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def authenticated?
|
22
|
+
resume_session
|
23
|
+
end
|
24
|
+
|
25
|
+
def require_authentication
|
26
|
+
resume_session || request_authentication
|
27
|
+
end
|
28
|
+
|
29
|
+
def resume_session
|
30
|
+
Current.session ||= Session.from_cookie(cookies)
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_authentication
|
34
|
+
session[:return_to_after_authenticating] = request.url
|
35
|
+
redirect_to root_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def after_authentication_url
|
39
|
+
session.delete(:return_to_after_authenticating) || root_url
|
40
|
+
end
|
41
|
+
|
42
|
+
def require_unauthenticated
|
43
|
+
return unless authenticated?
|
44
|
+
|
45
|
+
redirect_to root_path, notice: "You are already signed in"
|
46
|
+
end
|
47
|
+
|
48
|
+
def start_new_session_for(person)
|
49
|
+
person.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
50
|
+
Current.session = session
|
51
|
+
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def terminate_session
|
56
|
+
resume_session&.destroy
|
57
|
+
cookies.delete(:session_id)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class RegistrationsController < ApplicationController
|
2
|
+
prevent_authenticated_access only: %i(new create)
|
3
|
+
allow_unauthenticated_access only: %i(new create)
|
4
|
+
|
5
|
+
def new
|
6
|
+
@person = Person.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def create
|
10
|
+
@person = Person.new(email_address: params[:person][:email_address])
|
11
|
+
|
12
|
+
if @person.valid?
|
13
|
+
EmailVerificationMailer.verify(@person.attributes).deliver_later
|
14
|
+
|
15
|
+
render :create
|
16
|
+
else
|
17
|
+
render :new, status: :unprocessable_entity
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/sessions_controller.rb.tt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
class SessionsController < ApplicationController
|
2
|
+
allow_unauthenticated_access
|
3
|
+
prevent_authenticated_access except: :destroy
|
4
|
+
|
5
|
+
def create
|
6
|
+
if (person = Person.authenticate_by(params:)).present?
|
7
|
+
start_new_session_for(person)
|
8
|
+
|
9
|
+
redirect_to after_authentication_url
|
10
|
+
else
|
11
|
+
render turbo_stream: turbo_stream.prepend_all("body", "<p>Something went wrong. Try again</p>")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy
|
16
|
+
terminate_session
|
17
|
+
|
18
|
+
redirect_to root_path
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Passkey < ApplicationRecord
|
2
|
+
include ActionView::Helpers::DateHelper
|
3
|
+
|
4
|
+
belongs_to :authenticatable, polymorphic: true
|
5
|
+
|
6
|
+
validates :external_id, presence: true, uniqueness: true
|
7
|
+
validates :public_key, presence: true
|
8
|
+
validates :sign_count, presence: true, numericality: {
|
9
|
+
only_integer: true, greater_than_or_equal_to: 0
|
10
|
+
}
|
11
|
+
|
12
|
+
before_destroy do
|
13
|
+
if authenticatable.passkeys.excluding(self).blank?
|
14
|
+
errors.add(:base, "Can't remove your only passkey")
|
15
|
+
throw :abort
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def name_or_default
|
20
|
+
name.presence || "security key"
|
21
|
+
end
|
22
|
+
|
23
|
+
def last_used
|
24
|
+
if last_used_at.present?
|
25
|
+
"Last used #{time_ago_in_words last_used_at} ago"
|
26
|
+
else
|
27
|
+
"Never used"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%%= link_to "Finish signing up", email_verification_url(@person.encode_webauthn_message) %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%%= email_verification_url(@person.encode_webauthn_message) %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%% if (prompt = prompt_for_new_passkey(callback: people_path)) %>
|
2
|
+
<h2>Verified!</h2>
|
3
|
+
<p>Let's add your passkey.</p>
|
4
|
+
|
5
|
+
<%%= prompt %>
|
6
|
+
<%% else %>
|
7
|
+
<h2>
|
8
|
+
Hmm, that link looks wrong.
|
9
|
+
<%%= link_to "Try again.", new_registration_path, class: "underline decoration-2" %>
|
10
|
+
</h2>
|
11
|
+
<%% end %>
|
data/lib/generators/has_secure_passkey/passkeys/templates/app/views/registrations/new.html.erb.tt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
<%%= form_with model: @person, url: registration_path do |f| %>
|
2
|
+
<%% if @person.errors.any? %>
|
3
|
+
<div style="color: red">
|
4
|
+
<h2><%%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved:</h2>
|
5
|
+
|
6
|
+
<ul>
|
7
|
+
<%% @person.errors.each do |error| %>
|
8
|
+
<li><%%= error.full_message %></li>
|
9
|
+
<%% end %>
|
10
|
+
</ul>
|
11
|
+
</div>
|
12
|
+
<%% end %>
|
13
|
+
|
14
|
+
<%%= f.email_field :email_address, autofocus: true, autocomplete: :email, placeholder: "Email" %>
|
15
|
+
<%%= f.submit "Sign up" %>
|
16
|
+
<%% end %>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# Preview all emails at http://localhost:3000/rails/mailers/email_verification_mailer
|
2
|
+
class EmailVerificationMailerPreview < ActionMailer::Preview
|
3
|
+
# Preview this email at http://localhost:3000/rails/mailers/email_verification_mailer/verify
|
4
|
+
def verify
|
5
|
+
EmailVerificationMailer.verify(Person.take.attributes)
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module HasSecurePasskey::ActiveRecordHelpers
|
2
|
+
def has_secure_passkey
|
3
|
+
encrypts :webauthn_id, deterministic: true
|
4
|
+
|
5
|
+
has_many :sessions, dependent: :destroy
|
6
|
+
has_many :passkeys, dependent: :delete_all, as: :authenticatable
|
7
|
+
validates :webauthn_id, uniqueness: true
|
8
|
+
|
9
|
+
accepts_nested_attributes_for :passkeys
|
10
|
+
|
11
|
+
before_validation do
|
12
|
+
self.webauthn_id ||= self.class.webauthn_id
|
13
|
+
end
|
14
|
+
|
15
|
+
define_singleton_method :webauthn_id do
|
16
|
+
WebAuthn.generate_user_id
|
17
|
+
end
|
18
|
+
|
19
|
+
define_singleton_method :authenticate_by do |params:|
|
20
|
+
HasSecurePasskey::AuthenticateBy.
|
21
|
+
new(model: self, params:).
|
22
|
+
authenticated
|
23
|
+
end
|
24
|
+
|
25
|
+
define_singleton_method :create_by_webauthn do |params:|
|
26
|
+
authenticatable = new(HasSecurePasskey::OptionsForCreate.
|
27
|
+
from_message(params[:webauthn_message]).authenticatable)
|
28
|
+
|
29
|
+
ActiveRecord::Base.transaction do
|
30
|
+
unless authenticatable.save && authenticatable.add_passkey(params:)
|
31
|
+
raise ActiveRecord::Rollback
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
authenticatable
|
36
|
+
end
|
37
|
+
|
38
|
+
define_method :add_passkey do |params:|
|
39
|
+
HasSecurePasskey::AddPasskey.
|
40
|
+
new(authenticatable: self, params:).
|
41
|
+
save
|
42
|
+
end
|
43
|
+
|
44
|
+
define_method :encode_webauthn_message do
|
45
|
+
HasSecurePasskey::OptionsForCreate.new(authenticatable: self).message
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class HasSecurePasskey::AddPasskey
|
2
|
+
def initialize(authenticatable:, params:)
|
3
|
+
@authenticatable = authenticatable
|
4
|
+
@params = params
|
5
|
+
end
|
6
|
+
|
7
|
+
def save
|
8
|
+
if valid?
|
9
|
+
passkey.save
|
10
|
+
passkey
|
11
|
+
else
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :authenticatable, :params
|
19
|
+
|
20
|
+
delegate_missing_to :credential
|
21
|
+
|
22
|
+
def credential
|
23
|
+
@credential ||= WebAuthn::Credential.from_create(params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
credential.verify(challenge)
|
28
|
+
rescue WebAuthn::Error => e
|
29
|
+
authenticatable.errors.add(:passkeys, e.message)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def passkey
|
34
|
+
@passkey ||= authenticatable.passkeys.build(
|
35
|
+
external_id: credential.id, public_key:, sign_count:
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def challenge
|
40
|
+
HasSecurePasskey::OptionsForCreate.
|
41
|
+
from_message(params[:webauthn_message]).challenge
|
42
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
43
|
+
""
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class HasSecurePasskey::AuthenticateBy
|
2
|
+
def initialize(model:, params:)
|
3
|
+
@model = model
|
4
|
+
@params = params
|
5
|
+
end
|
6
|
+
|
7
|
+
def authenticated
|
8
|
+
return @authenticated if defined?(@authenticated)
|
9
|
+
|
10
|
+
@authenticated =
|
11
|
+
if valid?
|
12
|
+
passkey.authenticatable
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :params, :model
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
passkey.present? && verified? &&
|
22
|
+
passkey.update(sign_count: webauthn_credential.sign_count) &&
|
23
|
+
passkey.touch(:last_used_at)
|
24
|
+
end
|
25
|
+
|
26
|
+
def webauthn_credential
|
27
|
+
@webauthn_credential ||= WebAuthn::Credential.from_get(params)
|
28
|
+
end
|
29
|
+
|
30
|
+
def passkey
|
31
|
+
@passkey ||= Passkey.find_by(
|
32
|
+
external_id: webauthn_credential.id, authenticatable_type: model.to_s
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def verified?
|
37
|
+
webauthn_credential.verify(challenge, public_key: passkey.public_key,
|
38
|
+
sign_count: passkey.sign_count)
|
39
|
+
rescue WebAuthn::Error, WebAuthn::SignCountVerificationError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def challenge
|
44
|
+
HasSecurePasskey::OptionsForGet.
|
45
|
+
from_message(params[:webauthn_message]).challenge
|
46
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
47
|
+
""
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module HasSecurePasskey
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace HasSecurePasskey
|
4
|
+
config.eager_load_namespaces << HasSecurePasskey
|
5
|
+
config.autoload_once_paths = %W(
|
6
|
+
#{root}/app/helpers
|
7
|
+
)
|
8
|
+
|
9
|
+
initializer "has_secure_passkey.webauthn" do |app|
|
10
|
+
WebAuthn.configure do |config|
|
11
|
+
config.origin = ENV.fetch("APP_URL", app.config.x.url)
|
12
|
+
config.rp_name = app.name
|
13
|
+
config.credential_options_timeout = 120_000
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
initializer "has_secure_passkey.assets" do
|
18
|
+
if Rails.application.config.respond_to?(:assets)
|
19
|
+
Rails.application.config.assets.precompile += %w(has_secure_passkey.js)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "has_secure_passkey.importmap", before: "importmap" do |app|
|
24
|
+
if Rails.application.respond_to?(:importmap)
|
25
|
+
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
initializer "has_secure_passkey.helpers", before: :load_config_initializers do
|
30
|
+
ActiveSupport.on_load(:action_controller_base) do
|
31
|
+
helper HasSecurePasskey::Engine.helpers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
initializer "has_secure_passkey.active_record_helpers" do
|
36
|
+
ActiveSupport.on_load(:active_record) do
|
37
|
+
extend HasSecurePasskey::ActiveRecordHelpers
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class HasSecurePasskey::OptionsForCreate
|
2
|
+
class << self
|
3
|
+
def verifier
|
4
|
+
Rails.application.message_verifier("passkey_signup")
|
5
|
+
end
|
6
|
+
|
7
|
+
def from_message(message)
|
8
|
+
new(**verifier.verify(message).symbolize_keys).tap do
|
9
|
+
_1.authenticatable.symbolize_keys! if _1.authenticatable.is_a?(Hash)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(authenticatable:, options: nil, challenge: nil)
|
15
|
+
@authenticatable = authenticatable
|
16
|
+
@options = options
|
17
|
+
@challenge = challenge
|
18
|
+
end
|
19
|
+
|
20
|
+
def message
|
21
|
+
self.class.verifier.generate(
|
22
|
+
{ challenge:, options:,
|
23
|
+
authenticatable: authenticatable.as_json(only: %i(email_address webauthn_id)) }.as_json
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def options
|
28
|
+
@options ||= { publicKey: credential }
|
29
|
+
end
|
30
|
+
|
31
|
+
def as_json
|
32
|
+
options
|
33
|
+
end
|
34
|
+
|
35
|
+
def challenge
|
36
|
+
@challenge ||= credential.challenge
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :authenticatable
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def credential
|
44
|
+
@credential ||= WebAuthn::Credential.options_for_create(
|
45
|
+
user: { name: authenticatable.email_address, id: authenticatable.webauthn_id },
|
46
|
+
exclude: authenticatable.passkeys.pluck(:external_id)
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class HasSecurePasskey::OptionsForGet
|
2
|
+
class << self
|
3
|
+
def verifier
|
4
|
+
Rails.application.message_verifier("passkey_login")
|
5
|
+
end
|
6
|
+
|
7
|
+
def from_message(message)
|
8
|
+
new(**verifier.verify(message).symbolize_keys)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(challenge: nil)
|
13
|
+
@challenge = challenge
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
self.class.verifier.generate({ challenge: })
|
18
|
+
end
|
19
|
+
|
20
|
+
def challenge
|
21
|
+
@challenge ||= credential.challenge
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_json
|
25
|
+
{ publicKey: credential.as_json }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def credential
|
31
|
+
@credential ||= WebAuthn::Credential.
|
32
|
+
options_for_get(user_verification: "discouraged")
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "webauthn"
|
2
|
+
|
3
|
+
require "has_secure_passkey/version"
|
4
|
+
require "has_secure_passkey/engine"
|
5
|
+
require "has_secure_passkey/options_for_create"
|
6
|
+
require "has_secure_passkey/options_for_get"
|
7
|
+
require "has_secure_passkey/active_record_helpers"
|
8
|
+
require "has_secure_passkey/authenticate_by"
|
9
|
+
require "has_secure_passkey/add_passkey"
|
10
|
+
|
11
|
+
module HasSecurePasskey
|
12
|
+
end
|