has_secure_passkey 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|