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.
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