has_secure_passkey 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +32 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/javascripts/@github--webauthn-json.js +1 -0
  6. data/app/assets/javascripts/@rails--request.js +2 -0
  7. data/app/assets/javascripts/components/has_secure_passkey.js +3 -0
  8. data/app/assets/javascripts/components/web_authn.js +81 -0
  9. data/app/assets/javascripts/has_secure_passkey.js +406 -0
  10. data/app/assets/stylesheets/has_secure_passkey/application.css +15 -0
  11. data/app/controllers/has_secure_passkey/application_controller.rb +4 -0
  12. data/app/controllers/has_secure_passkey/challenges_controller.rb +6 -0
  13. data/app/helpers/has_secure_passkey/application_helper.rb +32 -0
  14. data/app/mailers/has_secure_passkey/application_mailer.rb +6 -0
  15. data/app/models/has_secure_passkey/application_record.rb +5 -0
  16. data/app/views/has_secure_passkey/challenges/create.turbo_stream.erb +5 -0
  17. data/app/views/layouts/has_secure_passkey/application.html.erb +17 -0
  18. data/config/importmap.rb +1 -0
  19. data/config/routes.rb +3 -0
  20. data/lib/generators/has_secure_passkey/passkeys/passkeys_generator.rb +59 -0
  21. data/lib/generators/has_secure_passkey/passkeys/templates/app/channels/application_cable/connection.rb.tt +16 -0
  22. data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/concerns/authentication.rb.tt +59 -0
  23. data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/email_verifications_controller.rb.tt +7 -0
  24. data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/people_controller.rb.tt +11 -0
  25. data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/registrations_controller.rb.tt +20 -0
  26. data/lib/generators/has_secure_passkey/passkeys/templates/app/controllers/sessions_controller.rb.tt +20 -0
  27. data/lib/generators/has_secure_passkey/passkeys/templates/app/mailers/email_verification_mailer.rb.tt +7 -0
  28. data/lib/generators/has_secure_passkey/passkeys/templates/app/models/current.rb.tt +4 -0
  29. data/lib/generators/has_secure_passkey/passkeys/templates/app/models/passkey.rb.tt +30 -0
  30. data/lib/generators/has_secure_passkey/passkeys/templates/app/models/person.rb.tt +9 -0
  31. data/lib/generators/has_secure_passkey/passkeys/templates/app/models/session.rb.tt +7 -0
  32. data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verification_mailer/verify.html.erb.tt +1 -0
  33. data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verification_mailer/verify.text.erb.tt +1 -0
  34. data/lib/generators/has_secure_passkey/passkeys/templates/app/views/email_verifications/show.html.erb.tt +11 -0
  35. data/lib/generators/has_secure_passkey/passkeys/templates/app/views/registrations/create.turbo_stream.erb.tt +7 -0
  36. data/lib/generators/has_secure_passkey/passkeys/templates/app/views/registrations/new.html.erb.tt +16 -0
  37. data/lib/generators/has_secure_passkey/passkeys/templates/test/mailers/previews/email_verification_mailer_preview.rb.tt +7 -0
  38. data/lib/has_secure_passkey/active_record_helpers.rb +48 -0
  39. data/lib/has_secure_passkey/add_passkey.rb +45 -0
  40. data/lib/has_secure_passkey/authenticate_by.rb +49 -0
  41. data/lib/has_secure_passkey/engine.rb +41 -0
  42. data/lib/has_secure_passkey/options_for_create.rb +49 -0
  43. data/lib/has_secure_passkey/options_for_get.rb +34 -0
  44. data/lib/has_secure_passkey/version.rb +3 -0
  45. data/lib/has_secure_passkey.rb +12 -0
  46. data/lib/tasks/has_secure_passkey_tasks.rake +5 -0
  47. 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>
@@ -0,0 +1 @@
1
+ pin "has_secure_passkey", to: "has_secure_passkey.js"
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ HasSecurePasskey::Engine.routes.draw do
2
+ post "/challenges/:session_path", to: "challenges#create", as: :challenges
3
+ end
@@ -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,7 @@
1
+ class EmailVerificationsController < ApplicationController
2
+ prevent_authenticated_access
3
+ allow_unauthenticated_access
4
+
5
+ def show
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ class PeopleController < ApplicationController
2
+ allow_unauthenticated_access
3
+ prevent_authenticated_access
4
+
5
+ def create
6
+ @person = Person.create_by_webauthn(params:)
7
+ start_new_session_for(@person)
8
+
9
+ redirect_to after_authentication_url
10
+ end
11
+ 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
@@ -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,7 @@
1
+ class EmailVerificationMailer < ApplicationMailer
2
+ def verify(person_attributes)
3
+ @person = Person.new(person_attributes)
4
+
5
+ mail to: @person.email_address, subject: "Verify your email"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+ delegate :person, to: :session, allow_nil: true
4
+ 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,9 @@
1
+ class Person < ApplicationRecord
2
+ has_secure_passkey
3
+
4
+ has_many :sessions, dependent: :destroy
5
+
6
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
7
+
8
+ validates :email_address, uniqueness: true, presence: true
9
+ end
@@ -0,0 +1,7 @@
1
+ class Session < ApplicationRecord
2
+ belongs_to :person
3
+
4
+ def self.from_cookie(cookie_jar)
5
+ find_by(id: cookie_jar.signed[:session_id])
6
+ end
7
+ 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 %>
@@ -0,0 +1,7 @@
1
+ <%%= turbo_stream.update "new-registration" do %>
2
+ <div>
3
+ <h2>We've sent you an email!</h2>
4
+
5
+ <p>Click the link in the email to verify it's you.</p>
6
+ </div>
7
+ <%% end %>
@@ -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,3 @@
1
+ module HasSecurePasskey
2
+ VERSION = "0.1.0"
3
+ 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
@@ -0,0 +1,5 @@
1
+ desc "Install passkeys"
2
+ task :passkeys do
3
+ Rails::Command.invoke :generate, [ "has_secure_passkey:passkeys" ]
4
+ Rails::Command.invoke "db:encryption:init"
5
+ end