passkeys-rails 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +17 -2
  4. data/app/controllers/passkeys/rails/application_controller.rb +24 -0
  5. data/app/controllers/passkeys/rails/passkeys_controller.rb +63 -0
  6. data/app/models/concerns/passkeys/rails/authenticatable.rb +19 -0
  7. data/app/models/passkeys/rails/agent.rb +17 -0
  8. data/app/models/passkeys/rails/application_record.rb +7 -0
  9. data/app/models/passkeys/rails/error.rb +16 -0
  10. data/app/models/passkeys/rails/passkey.rb +10 -0
  11. data/config/routes.rb +1 -1
  12. data/lib/generators/passkeys_rails/USAGE +1 -1
  13. data/lib/generators/passkeys_rails/install_generator.rb +14 -12
  14. data/lib/generators/passkeys_rails/templates/README +2 -2
  15. data/lib/generators/passkeys_rails/templates/passkeys_rails_config.rb +2 -2
  16. data/lib/passkeys/rails/controllers/helpers.rb +33 -0
  17. data/lib/passkeys/rails/engine.rb +44 -0
  18. data/lib/passkeys/rails/interactors/begin_authentication.rb +11 -0
  19. data/lib/passkeys/rails/interactors/begin_challenge.rb +37 -0
  20. data/lib/passkeys/rails/interactors/begin_registration.rb +25 -0
  21. data/lib/passkeys/rails/interactors/finish_authentication.rb +55 -0
  22. data/lib/passkeys/rails/interactors/finish_registration.rb +79 -0
  23. data/lib/passkeys/rails/interactors/generate_auth_token.rb +29 -0
  24. data/lib/passkeys/rails/interactors/refresh_token.rb +19 -0
  25. data/lib/passkeys/rails/interactors/validate_auth_token.rb +35 -0
  26. data/lib/passkeys/rails/railtie.rb +19 -0
  27. data/lib/passkeys/rails/version.rb +5 -0
  28. data/lib/passkeys-rails.rb +36 -0
  29. metadata +24 -25
  30. data/app/controllers/passkeys_rails/application_controller.rb +0 -22
  31. data/app/controllers/passkeys_rails/passkeys_controller.rb +0 -61
  32. data/app/helpers/passkeys_rails/application_helper.rb +0 -21
  33. data/app/helpers/passkeys_rails/passkeys_helper.rb +0 -4
  34. data/app/interactors/passkeys_rails/begin_authentication.rb +0 -9
  35. data/app/interactors/passkeys_rails/begin_challenge.rb +0 -35
  36. data/app/interactors/passkeys_rails/begin_registration.rb +0 -23
  37. data/app/interactors/passkeys_rails/finish_authentication.rb +0 -53
  38. data/app/interactors/passkeys_rails/finish_registration.rb +0 -77
  39. data/app/interactors/passkeys_rails/generate_auth_token.rb +0 -27
  40. data/app/interactors/passkeys_rails/refresh_token.rb +0 -17
  41. data/app/interactors/passkeys_rails/validate_auth_token.rb +0 -33
  42. data/app/models/concerns/passkeys_rails/authenticatable.rb +0 -17
  43. data/app/models/passkeys_rails/agent.rb +0 -15
  44. data/app/models/passkeys_rails/application_record.rb +0 -5
  45. data/app/models/passkeys_rails/error.rb +0 -14
  46. data/app/models/passkeys_rails/passkey.rb +0 -8
  47. data/lib/passkeys_rails/engine.rb +0 -24
  48. data/lib/passkeys_rails/error_middleware.rb +0 -17
  49. data/lib/passkeys_rails/railtie.rb +0 -17
  50. data/lib/passkeys_rails/version.rb +0 -3
  51. data/lib/passkeys_rails.rb +0 -38
  52. /data/app/views/layouts/{passkeys_rails → passkeys/rails}/application.html.erb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e23ace0ad232fd6a7a54c8f69ff05178b15cbd518bd06228430cd9fde0fdb496
4
- data.tar.gz: 5f7f5e08e273e0192e036b0015f3d3d27fa42181af2c5394a267b937fc29a197
3
+ metadata.gz: c6b2553d498c30365341aba0aad49eafd09c5b49dd5453bbad29c20a026ab39c
4
+ data.tar.gz: 795d362e15d977c47b5381a4fe5cc852cdd546a0fa8f81e5a5452343f2b4fa1e
5
5
  SHA512:
6
- metadata.gz: a669fc055ae57baa578661cda651ea6938e7d77d6d294d8edbb4fc4c9662bfcbb44d385901bb3e891251b5b137d83f1d6ea304d16eb73b2c238f7ef306cf313e
7
- data.tar.gz: b1b6334a3a2e586458394aae70d3da78c2b00126b1a52dfebc709311e22673207b47ce44a519168ca8252e18337b02cfd9b0f338ac748613c7fceb62d5675a44
6
+ metadata.gz: 15694fd664f6b60343ede055acfefdfef0d8378ffe386420e7cd5d996920fe19a9321ac1148f3e22605b85a8486cbd1f56999167a84be33f9acf950ea0e5e722
7
+ data.tar.gz: b6f224af15ee0feb2489c786eefe68293d4854b43b7dcf3a2d17dea4514497cdd5df5f80bd5941972918b4c05864529c62bbe694c09ff58540a87529216bf8f3
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ ### 0.1.2
2
+
3
+ * Restructured lib directory.
4
+
5
+ * Fixed naming convention for gem/gemspec.
6
+
7
+ * Fixed exception handling.
8
+
9
+ ### 0.1.1
10
+
11
+ * Fixed dependency
12
+
13
+ ### 0.1.0
14
+
15
+ * Initial release - looking for feedback
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
+ [![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg)](https://badge.fury.io/rb/passkeys-rails)
1
2
  [![Build Status](https://app.travis-ci.com/alliedcode/passkeys-rails.svg?branch=main)](https://travis-ci.org/alliedcode/passkeys-rails)
2
3
  [![codecov](https://codecov.io/gh/alliedcode/passkeys-rails/branch/main/graph/badge.svg?token=UHSNJDUL21)](https://codecov.io/gh/alliedcode/passkeys-rails)
3
4
 
4
- # PasskeysRails
5
+ # Passkeys::Rails
5
6
  Devise is awesome, but we don't need all that UI/UX for PassKeys. This gem is to make it easy to provide a back end that authenticates a mobile front end with PassKeys.
6
7
 
7
8
  ## Usage
8
9
  rails passkeys-rails::install
9
- PasskeysRails maintains an Agent model and related Passeys. If you have a user model, add `include PasskeysRails::Authenticatable` to your model and include the name of that class (e.g. "User") in the authenticatable_class param when calling the register API.
10
+ Passkeys::Rails maintains an Agent model and related Passeys. If you have a user model, add `include Passkeys::Rails::Authenticatable` to your model and include the name of that class (e.g. "User") in the authenticatable_class param when calling the register API.
10
11
 
11
12
  ## Installation
12
13
  Add this line to your application's Gemfile:
@@ -25,6 +26,20 @@ Or install it yourself as:
25
26
  $ gem install passkeys_rails
26
27
  ```
27
28
 
29
+ Depending on your application's configuration some manual setup may be required:
30
+
31
+ 1. Add a before_action to all controllers that require authentication to use.
32
+
33
+ For example:
34
+
35
+ before_action :authenticate_passkey!, except: [:index]
36
+
37
+ 2. Optionally include Passkeys::Rails::Authenticatable to the model(s) you are using as
38
+ your user model(s). For example, the User model.
39
+
40
+ 3. See the reference mobile applications for how to use passkeys-rails for passkey
41
+ authentication.
42
+
28
43
  ## Contributing
29
44
  Contribution directions go here.
30
45
 
@@ -0,0 +1,24 @@
1
+ module Passkeys
2
+ module Rails
3
+ class ApplicationController < ActionController::Base
4
+ rescue_from ::Interactor::Failure, with: :handle_interactor_failure
5
+ rescue_from ActionController::ParameterMissing, with: :handle_missing_parameter
6
+
7
+ protected
8
+
9
+ def handle_missing_parameter(error)
10
+ render_error(:authentication, 'missing_parameter', error.message)
11
+ end
12
+
13
+ def handle_interactor_failure(failure)
14
+ render_error(:authentication, failure.context.code, failure.context.message)
15
+ end
16
+
17
+ private
18
+
19
+ def render_error(context, code, message, status: :unprocessable_entity)
20
+ render json: { error: { context:, code:, message: } }, status:
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ module Passkeys
2
+ module Rails
3
+ class PasskeysController < ApplicationController
4
+ def challenge
5
+ result = Passkeys::Rails::BeginChallenge.call!(username: challenge_params[:username])
6
+
7
+ # Store the challenge so we can verify the future register or authentication request
8
+ session[:passkeys_rails] = result.session_data
9
+
10
+ render json: result.response.as_json
11
+ end
12
+
13
+ def register
14
+ result = Passkeys::Rails::FinishRegistration.call!(credential: attestation_credential_params.to_h,
15
+ authenticatable_class:,
16
+ username: session.dig(:passkeys_rails, :username),
17
+ challenge: session.dig(:passkeys_rails, :challenge))
18
+
19
+ render json: { username: result.username, auth_token: result.auth_token }
20
+ end
21
+
22
+ def authenticate
23
+ result = Passkeys::Rails::FinishAuthentication.call!(credential: authentication_params.to_h,
24
+ challenge: session.dig(:passkeys_rails, :challenge))
25
+
26
+ render json: { username: result.username, auth_token: result.auth_token }
27
+ end
28
+
29
+ def refresh
30
+ result = Passkeys::Rails::RefreshToken.call!(token: refresh_params[:auth_token])
31
+ render json: { username: result.username, auth_token: result.auth_token }
32
+ end
33
+
34
+ protected
35
+
36
+ def challenge_params
37
+ params.permit(:username)
38
+ end
39
+
40
+ def attestation_credential_params
41
+ credential = params.require(:credential)
42
+ credential.require(%i[id rawId type response])
43
+ credential.require(:response).require(%i[attestationObject clientDataJSON])
44
+ credential.permit(:id, :rawId, :type, { response: %i[attestationObject clientDataJSON] })
45
+ end
46
+
47
+ def authenticatable_class
48
+ params[:authenticatable_class]
49
+ end
50
+
51
+ def authentication_params
52
+ params.require(%i[id rawId type response])
53
+ params.require(:response).require(%i[authenticatorData clientDataJSON signature userHandle])
54
+ params.permit(:id, :rawId, :type, { response: %i[authenticatorData clientDataJSON signature userHandle] })
55
+ end
56
+
57
+ def refresh_params
58
+ params.require(:auth_token)
59
+ params.permit(:auth_token)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_support/concern'
2
+
3
+ module Passkeys
4
+ module Rails
5
+ module Authenticatable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ has_one :agent, as: :authenticatable
10
+
11
+ delegate :registered?, to: :agent, allow_nil: true
12
+
13
+ def registering_with(_agent)
14
+ # initialize required attributes
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Passkeys
2
+ module Rails
3
+ class Agent < ApplicationRecord
4
+ belongs_to :authenticatable, polymorphic: true, optional: true
5
+ has_many :passkeys
6
+
7
+ scope :registered, -> { where.not registered_at: nil }
8
+ scope :unregistered, -> { where registered_at: nil }
9
+
10
+ validates :username, presence: true, uniqueness: true
11
+
12
+ def registered?
13
+ registered_at.present?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ module Passkeys
2
+ module Rails
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Passkeys
2
+ module Rails
3
+ class Error < StandardError
4
+ attr_reader :hash
5
+
6
+ def initialize(message, hash = {})
7
+ @hash = hash
8
+ super(message)
9
+ end
10
+
11
+ def to_h
12
+ { error: hash.merge(context: message) }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Passkeys
2
+ module Rails
3
+ class Passkey < ApplicationRecord
4
+ belongs_to :agent
5
+ validates :identifier, presence: true, uniqueness: true
6
+ validates :public_key, presence: true
7
+ validates :sign_count, presence: true
8
+ end
9
+ end
10
+ end
data/config/routes.rb CHANGED
@@ -1,4 +1,4 @@
1
- PasskeysRails::Engine.routes.draw do
1
+ Passkeys::Rails::Engine.routes.draw do
2
2
  post 'passkeys/challenge'
3
3
  post 'passkeys/register'
4
4
  post 'passkeys/authenticate'
@@ -1,5 +1,5 @@
1
1
  Description:
2
- Creates a PasskeysRails config file, updates the routes and adds migrations.
2
+ Creates a Passkeys::Rails config file, updates the routes and adds migrations.
3
3
 
4
4
  Example:
5
5
  bin/rails generate passkeys-rails:install
@@ -1,20 +1,22 @@
1
1
  require 'rails/generators'
2
2
 
3
- module PasskeysRails
4
- module Generators
5
- class InstallGenerator < Rails::Generators::Base
6
- source_root File.expand_path("templates", __dir__)
3
+ module Passkeys
4
+ module Rails
5
+ module Generators
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
7
8
 
8
- def copy_config
9
- template 'passkeys_rails_config.rb', "config/initializers/passkeys_rails.rb"
10
- end
9
+ def copy_config
10
+ template 'passkeys_rails_config.rb', "config/initializers/passkeys_rails.rb"
11
+ end
11
12
 
12
- def add_routes
13
- route 'mount PasskeysRails::Engine => "/passkeys_rails"'
14
- end
13
+ def add_routes
14
+ route 'mount Passkeys::Rails::Engine => "/passkeys_rails"'
15
+ end
15
16
 
16
- def show_readme
17
- readme "README" if behavior == :invoke
17
+ def show_readme
18
+ readme "README" if behavior == :invoke
19
+ end
18
20
  end
19
21
  end
20
22
  end
@@ -6,9 +6,9 @@ Depending on your application's configuration some manual setup may be required:
6
6
 
7
7
  For example:
8
8
 
9
- before_action :authitencate_passkey!, except: [:index]
9
+ before_action :authenticate_passkey!, except: [:index]
10
10
 
11
- 2. Optionally include PasskeysRails::Authenticatable to the model(s) you are using as
11
+ 2. Optionally include Passkeys::Rails::Authenticatable to the model(s) you are using as
12
12
  your user model(s). For example, the User model.
13
13
 
14
14
  3. See the reference mobile applications for how to use passkeys-rails for passkey
@@ -1,6 +1,6 @@
1
- require 'passkeys_rails'
1
+ require 'passkeys-rails'
2
2
 
3
- PasskeysRails.config do |c|
3
+ Passkeys::Rails.config do |c|
4
4
  # Secret used to encode the auth token.
5
5
  # Changing this value will invalidate all tokens that have been fetched
6
6
  # through the API.
@@ -0,0 +1,33 @@
1
+ module Passkeys
2
+ module Rails
3
+ module Controllers
4
+ module Helpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from Passkeys::Rails::Error do |e|
9
+ render json: e.to_h, status: :unauthorized
10
+ end
11
+ end
12
+
13
+ def current_agent
14
+ return nil if request.headers['HTTP_X_AUTH'].blank?
15
+
16
+ @current_agent ||= validated_auth_token&.success? && validated_auth_token&.agent
17
+ end
18
+
19
+ def authenticate_passkey!
20
+ return if validated_auth_token.success?
21
+
22
+ raise Passkeys::Rails::Error.new(:authentication,
23
+ code: :unauthorized,
24
+ message: "You are not authorized to access this resource.")
25
+ end
26
+
27
+ def validated_auth_token
28
+ @validated_auth_token ||= Passkeys::Rails::ValidateAuthToken.call(auth_token: request.headers['HTTP_X_AUTH'])
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ require "rails"
2
+ require "active_support/core_ext/numeric/time"
3
+ require "active_support/dependencies"
4
+
5
+ require "interactor"
6
+ require "jwt"
7
+ require "webauthn"
8
+
9
+ require_relative 'controllers/helpers'
10
+ require_relative "interactors/begin_authentication"
11
+ require_relative "interactors/begin_challenge"
12
+ require_relative "interactors/begin_registration"
13
+ require_relative "interactors/finish_authentication"
14
+ require_relative "interactors/finish_registration"
15
+ require_relative "interactors/generate_auth_token"
16
+ require_relative "interactors/refresh_token"
17
+ require_relative "interactors/validate_auth_token"
18
+
19
+ module Passkeys
20
+ module Rails
21
+ class Engine < ::Rails::Engine
22
+ isolate_namespace Passkeys::Rails
23
+
24
+ config.generators do |g|
25
+ g.test_framework :rspec
26
+ g.fixture_replacement :factory_bot
27
+ g.factory_bot dir: 'spec/factories'
28
+ g.assets false
29
+ g.helper false
30
+ end
31
+
32
+ # Include helpers
33
+ initializer "passkeys-rails.helpers" do
34
+ ActiveSupport.on_load(:action_controller_base) do
35
+ include Passkeys::Rails::Controllers::Helpers
36
+ end
37
+
38
+ ActiveSupport.on_load(:action_controller_api) do
39
+ include Passkeys::Rails::Controllers::Helpers
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ module Passkeys
2
+ module Rails
3
+ class BeginAuthentication
4
+ include Interactor
5
+
6
+ def call
7
+ context.options = WebAuthn::Credential.options_for_get
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ module Passkeys
2
+ module Rails
3
+ class BeginChallenge
4
+ include Interactor
5
+
6
+ delegate :username, to: :context
7
+
8
+ def call
9
+ result = generate_challenge!
10
+
11
+ options = result.options
12
+
13
+ context.response = options
14
+ context.session_data = session_data(options)
15
+ rescue Interactor::Failure => e
16
+ context.fail! code: e.context.code, message: e.context.message
17
+ end
18
+
19
+ private
20
+
21
+ def generate_challenge!
22
+ if username.present?
23
+ BeginRegistration.call!(username:)
24
+ else
25
+ BeginAuthentication.call!
26
+ end
27
+ end
28
+
29
+ def session_data(options)
30
+ {
31
+ username:,
32
+ challenge: WebAuthn.standard_encoder.encode(options.challenge)
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module Passkeys
2
+ module Rails
3
+ class BeginRegistration
4
+ include Interactor
5
+
6
+ delegate :username, to: :context
7
+
8
+ def call
9
+ agent = create_unregistered_agent
10
+
11
+ context.options = WebAuthn::Credential.options_for_create(user: { id: agent.webauthn_identifier, name: agent.username })
12
+ end
13
+
14
+ private
15
+
16
+ def create_unregistered_agent
17
+ agent = Agent.create(username:, webauthn_identifier: WebAuthn.generate_user_id)
18
+
19
+ context.fail!(code: :validation_errors, message: agent.errors.full_messages.to_sentence) unless agent.valid?
20
+
21
+ agent
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ # Finish authentication ceremony
2
+ module Passkeys
3
+ module Rails
4
+ class FinishAuthentication
5
+ include Interactor
6
+
7
+ delegate :credential, :challenge, to: :context
8
+
9
+ def call
10
+ verify_credential!
11
+
12
+ context.username = agent.username
13
+ context.auth_token = GenerateAuthToken.call!(agent:).auth_token
14
+ rescue Interactor::Failure => e
15
+ context.fail! code: e.context.code, message: e.context.message
16
+ end
17
+
18
+ private
19
+
20
+ def verify_credential!
21
+ webauthn_credential.verify(
22
+ challenge,
23
+ public_key: passkey.public_key,
24
+ sign_count: passkey.sign_count
25
+ )
26
+
27
+ passkey.update!(sign_count: webauthn_credential.sign_count)
28
+ agent.update!(last_authenticated_at: Time.current)
29
+ rescue WebAuthn::SignCountVerificationError
30
+ # Cryptographic verification of the authenticator data succeeded, but the signature counter was less than or equal
31
+ # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
32
+ # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
33
+ rescue WebAuthn::Error => e
34
+ context.fail!(code: :webauthn_error, message: e.message)
35
+ end
36
+
37
+ def webauthn_credential
38
+ @webauthn_credential ||= WebAuthn::Credential.from_get(credential)
39
+ end
40
+
41
+ def passkey
42
+ @passkey ||= begin
43
+ passkey = Passkey.find_by(identifier: webauthn_credential.id)
44
+ context.fail!(code: :passkey_not_found, message: "Unable to find the specified passkey") if passkey.blank?
45
+
46
+ passkey
47
+ end
48
+ end
49
+
50
+ def agent
51
+ passkey.agent
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,79 @@
1
+ # Finish registration ceremony
2
+ module Passkeys
3
+ module Rails
4
+ class FinishRegistration
5
+ include Interactor
6
+
7
+ delegate :credential, :username, :challenge, :authenticatable_class, to: :context
8
+
9
+ def call
10
+ verify_credential!
11
+ store_passkey_and_register_agent!
12
+
13
+ context.username = agent.username
14
+ context.auth_token = GenerateAuthToken.call!(agent:).auth_token
15
+ rescue Interactor::Failure => e
16
+ context.fail! code: e.context.code, message: e.context.message
17
+ end
18
+
19
+ private
20
+
21
+ def verify_credential!
22
+ webauthn_credential.verify(challenge)
23
+ rescue WebAuthn::Error => e
24
+ context.fail!(code: :webauthn_error, message: e.message)
25
+ rescue StandardError => e
26
+ context.fail!(code: :error, message: e.message)
27
+ end
28
+
29
+ def store_passkey_and_register_agent!
30
+ agent.transaction do
31
+ begin
32
+ # Store Credential ID, Credential Public Key and Sign Count for future authentications
33
+ agent.passkeys.create!(
34
+ identifier: webauthn_credential.id,
35
+ public_key: webauthn_credential.public_key,
36
+ sign_count: webauthn_credential.sign_count
37
+ )
38
+
39
+ agent.update! registered_at: Time.current
40
+ rescue StandardError => e
41
+ context.fail! code: :passkey_error, message: e.message
42
+ end
43
+
44
+ create_authenticatable! if authenticatable_class.present?
45
+ end
46
+ end
47
+
48
+ def create_authenticatable!
49
+ klass = begin
50
+ authenticatable_class.constantize
51
+ rescue StandardError
52
+ context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{authenticatable_class}) is not defined")
53
+ end
54
+
55
+ begin
56
+ authenticatable = klass.create! do |obj|
57
+ obj.registering_with(agent) if obj.respond_to?(:registering_with)
58
+ end
59
+ agent.update!(authenticatable:)
60
+ rescue ActiveRecord::RecordInvalid => e
61
+ context.fail!(code: :record_invalid, message: e.message)
62
+ end
63
+ end
64
+
65
+ def webauthn_credential
66
+ @webauthn_credential ||= WebAuthn::Credential.from_create(credential)
67
+ end
68
+
69
+ def agent
70
+ @agent ||= begin
71
+ agent = Agent.find_by(username:)
72
+ context.fail!(code: :agent_not_found, message: "Agent not found for session value: \"#{username}\"") if agent.blank?
73
+
74
+ agent
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,29 @@
1
+ module Passkeys
2
+ module Rails
3
+ class GenerateAuthToken
4
+ include Interactor
5
+
6
+ delegate :agent, to: :context
7
+
8
+ def call
9
+ context.auth_token = generate_auth_token
10
+ end
11
+
12
+ private
13
+
14
+ def generate_auth_token
15
+ JWT.encode(jwt_payload,
16
+ Passkeys::Rails.auth_token_secret,
17
+ Passkeys::Rails.auth_token_algorithm)
18
+ end
19
+
20
+ def jwt_payload
21
+ expiration = (Time.current + Passkeys::Rails.auth_token_expires_in).to_i
22
+
23
+ payload = { agent_id: agent.id }
24
+ payload[:exp] = expiration unless expiration.zero?
25
+ payload
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # Finish authentication ceremony
2
+ module Passkeys
3
+ module Rails
4
+ class RefreshToken
5
+ include Interactor
6
+
7
+ delegate :token, to: :context
8
+
9
+ def call
10
+ agent = ValidateAuthToken.call!(auth_token: token).agent
11
+
12
+ context.username = agent.username
13
+ context.auth_token = GenerateAuthToken.call!(agent:).auth_token
14
+ rescue Interactor::Failure => e
15
+ context.fail! code: e.context.code, message: e.context.message
16
+ end
17
+ end
18
+ end
19
+ end