passkeys-rails 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +1 -1
  4. data/app/controllers/concerns/passkeys/rails/authentication.rb +29 -0
  5. data/app/interactors/passkeys/rails/begin_authentication.rb +9 -0
  6. data/app/interactors/passkeys/rails/begin_challenge.rb +35 -0
  7. data/app/interactors/passkeys/rails/begin_registration.rb +23 -0
  8. data/app/interactors/passkeys/rails/finish_authentication.rb +53 -0
  9. data/app/interactors/passkeys/rails/finish_registration.rb +77 -0
  10. data/app/interactors/passkeys/rails/generate_auth_token.rb +27 -0
  11. data/app/interactors/passkeys/rails/refresh_token.rb +17 -0
  12. data/app/interactors/passkeys/rails/validate_auth_token.rb +33 -0
  13. data/app/models/concerns/passkeys/rails/authenticatable.rb +8 -10
  14. data/app/models/passkeys/rails/agent.rb +9 -11
  15. data/app/models/passkeys/rails/application_record.rb +3 -5
  16. data/app/models/passkeys/rails/error.rb +9 -11
  17. data/app/models/passkeys/rails/passkey.rb +6 -8
  18. data/config/initializers/application_controller.rb +12 -0
  19. data/lib/passkeys/rails/engine.rb +9 -32
  20. data/lib/passkeys/rails/version.rb +1 -1
  21. metadata +12 -11
  22. data/lib/passkeys/rails/controllers/helpers.rb +0 -33
  23. data/lib/passkeys/rails/interactors/begin_authentication.rb +0 -11
  24. data/lib/passkeys/rails/interactors/begin_challenge.rb +0 -37
  25. data/lib/passkeys/rails/interactors/begin_registration.rb +0 -25
  26. data/lib/passkeys/rails/interactors/finish_authentication.rb +0 -55
  27. data/lib/passkeys/rails/interactors/finish_registration.rb +0 -79
  28. data/lib/passkeys/rails/interactors/generate_auth_token.rb +0 -29
  29. data/lib/passkeys/rails/interactors/refresh_token.rb +0 -19
  30. data/lib/passkeys/rails/interactors/validate_auth_token.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6b2553d498c30365341aba0aad49eafd09c5b49dd5453bbad29c20a026ab39c
4
- data.tar.gz: 795d362e15d977c47b5381a4fe5cc852cdd546a0fa8f81e5a5452343f2b4fa1e
3
+ metadata.gz: 84469b812d394ac75533be6f935292932f43fceab669d0842be1daa077abe7ee
4
+ data.tar.gz: 8430b3679760bc10af93da66835b549646e50bca94bb25fc008ec067a37e32b0
5
5
  SHA512:
6
- metadata.gz: 15694fd664f6b60343ede055acfefdfef0d8378ffe386420e7cd5d996920fe19a9321ac1148f3e22605b85a8486cbd1f56999167a84be33f9acf950ea0e5e722
7
- data.tar.gz: b6f224af15ee0feb2489c786eefe68293d4854b43b7dcf3a2d17dea4514497cdd5df5f80bd5941972918b4c05864529c62bbe694c09ff58540a87529216bf8f3
6
+ metadata.gz: 654a20573b11dae74154bc3b8576f7204d2694218732ec88fdb80004286d20dc8ef0426fbf2f186efc33645ff9ce8604e32cdf59cae9b77712af271255807103
7
+ data.tar.gz: cb5380598b67e0969ec619570729cea1e534a8e100942adbb9d4389ce9b1eba0937e35d359dd3d8eb2646965e5343519c5404eccefbe606fed1f5a25d9ca6b08
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 0.1.3
2
+
3
+ * More restructuring and fixed issue where autoloading failed
4
+ during client app initialization.
5
+
1
6
  ### 0.1.2
2
7
 
3
8
  * Restructured lib directory.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg)](https://badge.fury.io/rb/passkeys-rails)
1
+ [![Gem Version](https://badge.fury.io/rb/passkeys-rails.svg?cachebust=1)](https://badge.fury.io/rb/passkeys-rails)
2
2
  [![Build Status](https://app.travis-ci.com/alliedcode/passkeys-rails.svg?branch=main)](https://travis-ci.org/alliedcode/passkeys-rails)
3
3
  [![codecov](https://codecov.io/gh/alliedcode/passkeys-rails/branch/main/graph/badge.svg?token=UHSNJDUL21)](https://codecov.io/gh/alliedcode/passkeys-rails)
4
4
 
@@ -0,0 +1,29 @@
1
+ module Passkeys::Rails
2
+ module Authentication
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ rescue_from Passkeys::Rails::Error do |e|
7
+ render json: e.to_h, status: :unauthorized
8
+ end
9
+ end
10
+
11
+ def current_agent
12
+ return nil if request.headers['HTTP_X_AUTH'].blank?
13
+
14
+ @current_agent ||= validated_auth_token&.success? && validated_auth_token&.agent
15
+ end
16
+
17
+ def authenticate_passkey!
18
+ return if validated_auth_token.success?
19
+
20
+ raise Passkeys::Rails::Error.new(:authentication,
21
+ code: :unauthorized,
22
+ message: "You are not authorized to access this resource.")
23
+ end
24
+
25
+ def validated_auth_token
26
+ @validated_auth_token ||= Passkeys::Rails::ValidateAuthToken.call(auth_token: request.headers['HTTP_X_AUTH'])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module Passkeys::Rails
2
+ class BeginAuthentication
3
+ include Interactor
4
+
5
+ def call
6
+ context.options = WebAuthn::Credential.options_for_get
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ module Passkeys::Rails
2
+ class BeginChallenge
3
+ include Interactor
4
+
5
+ delegate :username, to: :context
6
+
7
+ def call
8
+ result = generate_challenge!
9
+
10
+ options = result.options
11
+
12
+ context.response = options
13
+ context.session_data = session_data(options)
14
+ rescue Interactor::Failure => e
15
+ context.fail! code: e.context.code, message: e.context.message
16
+ end
17
+
18
+ private
19
+
20
+ def generate_challenge!
21
+ if username.present?
22
+ BeginRegistration.call!(username:)
23
+ else
24
+ BeginAuthentication.call!
25
+ end
26
+ end
27
+
28
+ def session_data(options)
29
+ {
30
+ username:,
31
+ challenge: WebAuthn.standard_encoder.encode(options.challenge)
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module Passkeys::Rails
2
+ class BeginRegistration
3
+ include Interactor
4
+
5
+ delegate :username, to: :context
6
+
7
+ def call
8
+ agent = create_unregistered_agent
9
+
10
+ context.options = WebAuthn::Credential.options_for_create(user: { id: agent.webauthn_identifier, name: agent.username })
11
+ end
12
+
13
+ private
14
+
15
+ def create_unregistered_agent
16
+ agent = Agent.create(username:, webauthn_identifier: WebAuthn.generate_user_id)
17
+
18
+ context.fail!(code: :validation_errors, message: agent.errors.full_messages.to_sentence) unless agent.valid?
19
+
20
+ agent
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ # Finish authentication ceremony
2
+ module Passkeys::Rails
3
+ class FinishAuthentication
4
+ include Interactor
5
+
6
+ delegate :credential, :challenge, to: :context
7
+
8
+ def call
9
+ verify_credential!
10
+
11
+ context.username = agent.username
12
+ context.auth_token = GenerateAuthToken.call!(agent:).auth_token
13
+ rescue Interactor::Failure => e
14
+ context.fail! code: e.context.code, message: e.context.message
15
+ end
16
+
17
+ private
18
+
19
+ def verify_credential!
20
+ webauthn_credential.verify(
21
+ challenge,
22
+ public_key: passkey.public_key,
23
+ sign_count: passkey.sign_count
24
+ )
25
+
26
+ passkey.update!(sign_count: webauthn_credential.sign_count)
27
+ agent.update!(last_authenticated_at: Time.current)
28
+ rescue WebAuthn::SignCountVerificationError
29
+ # Cryptographic verification of the authenticator data succeeded, but the signature counter was less than or equal
30
+ # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
31
+ # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
32
+ rescue WebAuthn::Error => e
33
+ context.fail!(code: :webauthn_error, message: e.message)
34
+ end
35
+
36
+ def webauthn_credential
37
+ @webauthn_credential ||= WebAuthn::Credential.from_get(credential)
38
+ end
39
+
40
+ def passkey
41
+ @passkey ||= begin
42
+ passkey = Passkey.find_by(identifier: webauthn_credential.id)
43
+ context.fail!(code: :passkey_not_found, message: "Unable to find the specified passkey") if passkey.blank?
44
+
45
+ passkey
46
+ end
47
+ end
48
+
49
+ def agent
50
+ passkey.agent
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,77 @@
1
+ # Finish registration ceremony
2
+ module Passkeys::Rails
3
+ class FinishRegistration
4
+ include Interactor
5
+
6
+ delegate :credential, :username, :challenge, :authenticatable_class, to: :context
7
+
8
+ def call
9
+ verify_credential!
10
+ store_passkey_and_register_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
+
18
+ private
19
+
20
+ def verify_credential!
21
+ webauthn_credential.verify(challenge)
22
+ rescue WebAuthn::Error => e
23
+ context.fail!(code: :webauthn_error, message: e.message)
24
+ rescue StandardError => e
25
+ context.fail!(code: :error, message: e.message)
26
+ end
27
+
28
+ def store_passkey_and_register_agent!
29
+ agent.transaction do
30
+ begin
31
+ # Store Credential ID, Credential Public Key and Sign Count for future authentications
32
+ agent.passkeys.create!(
33
+ identifier: webauthn_credential.id,
34
+ public_key: webauthn_credential.public_key,
35
+ sign_count: webauthn_credential.sign_count
36
+ )
37
+
38
+ agent.update! registered_at: Time.current
39
+ rescue StandardError => e
40
+ context.fail! code: :passkey_error, message: e.message
41
+ end
42
+
43
+ create_authenticatable! if authenticatable_class.present?
44
+ end
45
+ end
46
+
47
+ def create_authenticatable!
48
+ klass = begin
49
+ authenticatable_class.constantize
50
+ rescue StandardError
51
+ context.fail!(code: :invalid_authenticatable_class, message: "authenticatable_class (#{authenticatable_class}) is not defined")
52
+ end
53
+
54
+ begin
55
+ authenticatable = klass.create! do |obj|
56
+ obj.registering_with(agent) if obj.respond_to?(:registering_with)
57
+ end
58
+ agent.update!(authenticatable:)
59
+ rescue ActiveRecord::RecordInvalid => e
60
+ context.fail!(code: :record_invalid, message: e.message)
61
+ end
62
+ end
63
+
64
+ def webauthn_credential
65
+ @webauthn_credential ||= WebAuthn::Credential.from_create(credential)
66
+ end
67
+
68
+ def agent
69
+ @agent ||= begin
70
+ agent = Agent.find_by(username:)
71
+ context.fail!(code: :agent_not_found, message: "Agent not found for session value: \"#{username}\"") if agent.blank?
72
+
73
+ agent
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ module Passkeys::Rails
2
+ class GenerateAuthToken
3
+ include Interactor
4
+
5
+ delegate :agent, to: :context
6
+
7
+ def call
8
+ context.auth_token = generate_auth_token
9
+ end
10
+
11
+ private
12
+
13
+ def generate_auth_token
14
+ JWT.encode(jwt_payload,
15
+ Passkeys::Rails.auth_token_secret,
16
+ Passkeys::Rails.auth_token_algorithm)
17
+ end
18
+
19
+ def jwt_payload
20
+ expiration = (Time.current + Passkeys::Rails.auth_token_expires_in).to_i
21
+
22
+ payload = { agent_id: agent.id }
23
+ payload[:exp] = expiration unless expiration.zero?
24
+ payload
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # Finish authentication ceremony
2
+ module Passkeys::Rails
3
+ class RefreshToken
4
+ include Interactor
5
+
6
+ delegate :token, to: :context
7
+
8
+ def call
9
+ agent = ValidateAuthToken.call!(auth_token: token).agent
10
+
11
+ context.username = agent.username
12
+ context.auth_token = GenerateAuthToken.call!(agent:).auth_token
13
+ rescue Interactor::Failure => e
14
+ context.fail! code: e.context.code, message: e.context.message
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module Passkeys::Rails
2
+ class ValidateAuthToken
3
+ include Interactor
4
+
5
+ delegate :auth_token, to: :context
6
+
7
+ def call
8
+ context.fail!(code: :missing_token, message: "X-Auth header is required") if auth_token.blank?
9
+
10
+ context.agent = fetch_agent
11
+ end
12
+
13
+ private
14
+
15
+ def fetch_agent
16
+ agent = Agent.find_by(id: payload['agent_id'])
17
+ context.fail!(code: :invalid_token, message: "Invalid token - no agent exists with agent_id") if agent.blank?
18
+
19
+ agent
20
+ end
21
+
22
+ def payload
23
+ JWT.decode(auth_token,
24
+ Passkeys::Rails.auth_token_secret,
25
+ true,
26
+ { required_claims: %w[exp agent_id], algorithm: Passkeys::Rails.auth_token_algorithm }).first
27
+ rescue JWT::ExpiredSignature
28
+ context.fail!(code: :expired_token, message: "The token has expired")
29
+ rescue StandardError => e
30
+ context.fail!(code: :token_error, message: e.message)
31
+ end
32
+ end
33
+ end
@@ -1,18 +1,16 @@
1
1
  require 'active_support/concern'
2
2
 
3
- module Passkeys
4
- module Rails
5
- module Authenticatable
6
- extend ActiveSupport::Concern
3
+ module Passkeys::Rails
4
+ module Authenticatable
5
+ extend ActiveSupport::Concern
7
6
 
8
- included do
9
- has_one :agent, as: :authenticatable
7
+ included do
8
+ has_one :agent, as: :authenticatable
10
9
 
11
- delegate :registered?, to: :agent, allow_nil: true
10
+ delegate :registered?, to: :agent, allow_nil: true
12
11
 
13
- def registering_with(_agent)
14
- # initialize required attributes
15
- end
12
+ def registering_with(_agent)
13
+ # initialize required attributes
16
14
  end
17
15
  end
18
16
  end
@@ -1,17 +1,15 @@
1
- module Passkeys
2
- module Rails
3
- class Agent < ApplicationRecord
4
- belongs_to :authenticatable, polymorphic: true, optional: true
5
- has_many :passkeys
1
+ module Passkeys::Rails
2
+ class Agent < ApplicationRecord
3
+ belongs_to :authenticatable, polymorphic: true, optional: true
4
+ has_many :passkeys
6
5
 
7
- scope :registered, -> { where.not registered_at: nil }
8
- scope :unregistered, -> { where registered_at: nil }
6
+ scope :registered, -> { where.not registered_at: nil }
7
+ scope :unregistered, -> { where registered_at: nil }
9
8
 
10
- validates :username, presence: true, uniqueness: true
9
+ validates :username, presence: true, uniqueness: true
11
10
 
12
- def registered?
13
- registered_at.present?
14
- end
11
+ def registered?
12
+ registered_at.present?
15
13
  end
16
14
  end
17
15
  end
@@ -1,7 +1,5 @@
1
- module Passkeys
2
- module Rails
3
- class ApplicationRecord < ActiveRecord::Base
4
- self.abstract_class = true
5
- end
1
+ module Passkeys::Rails
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
6
4
  end
7
5
  end
@@ -1,16 +1,14 @@
1
- module Passkeys
2
- module Rails
3
- class Error < StandardError
4
- attr_reader :hash
1
+ module Passkeys::Rails
2
+ class Error < StandardError
3
+ attr_reader :hash
5
4
 
6
- def initialize(message, hash = {})
7
- @hash = hash
8
- super(message)
9
- end
5
+ def initialize(message, hash = {})
6
+ @hash = hash
7
+ super(message)
8
+ end
10
9
 
11
- def to_h
12
- { error: hash.merge(context: message) }
13
- end
10
+ def to_h
11
+ { error: hash.merge(context: message) }
14
12
  end
15
13
  end
16
14
  end
@@ -1,10 +1,8 @@
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
1
+ module Passkeys::Rails
2
+ class Passkey < ApplicationRecord
3
+ belongs_to :agent
4
+ validates :identifier, presence: true, uniqueness: true
5
+ validates :public_key, presence: true
6
+ validates :sign_count, presence: true
9
7
  end
10
8
  end
@@ -0,0 +1,12 @@
1
+ # These should be autoloaded, but if these aren't required here, apps using this
2
+ # gem will throw an exception that Passkeys::Rails::Authentication can't be found
3
+ require_relative '../../app/controllers/concerns/passkeys/rails/authentication'
4
+ require_relative '../../app/models/passkeys/rails/error'
5
+
6
+ class ActionController::Base
7
+ include Passkeys::Rails::Authentication
8
+ end
9
+
10
+ class ActionController::API
11
+ include Passkeys::Rails::Authentication
12
+ end
@@ -6,39 +6,16 @@ require "interactor"
6
6
  require "jwt"
7
7
  require "webauthn"
8
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"
9
+ module Passkeys::Rails
10
+ class Engine < ::Rails::Engine
11
+ isolate_namespace Passkeys::Rails
18
12
 
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
13
+ config.generators do |g|
14
+ g.test_framework :rspec
15
+ g.fixture_replacement :factory_bot
16
+ g.factory_bot dir: 'spec/factories'
17
+ g.assets false
18
+ g.helper false
42
19
  end
43
20
  end
44
21
  end
@@ -1,5 +1,5 @@
1
1
  module Passkeys
2
2
  module Rails
3
- VERSION = "0.1.2".freeze
3
+ VERSION = "0.1.3".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passkeys-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Troy Anderson
@@ -353,14 +353,24 @@ files:
353
353
  - Rakefile
354
354
  - app/assets/config/passkeys_rails_manifest.js
355
355
  - app/assets/stylesheets/passkeys_rails/application.css
356
+ - app/controllers/concerns/passkeys/rails/authentication.rb
356
357
  - app/controllers/passkeys/rails/application_controller.rb
357
358
  - app/controllers/passkeys/rails/passkeys_controller.rb
359
+ - app/interactors/passkeys/rails/begin_authentication.rb
360
+ - app/interactors/passkeys/rails/begin_challenge.rb
361
+ - app/interactors/passkeys/rails/begin_registration.rb
362
+ - app/interactors/passkeys/rails/finish_authentication.rb
363
+ - app/interactors/passkeys/rails/finish_registration.rb
364
+ - app/interactors/passkeys/rails/generate_auth_token.rb
365
+ - app/interactors/passkeys/rails/refresh_token.rb
366
+ - app/interactors/passkeys/rails/validate_auth_token.rb
358
367
  - app/models/concerns/passkeys/rails/authenticatable.rb
359
368
  - app/models/passkeys/rails/agent.rb
360
369
  - app/models/passkeys/rails/application_record.rb
361
370
  - app/models/passkeys/rails/error.rb
362
371
  - app/models/passkeys/rails/passkey.rb
363
372
  - app/views/layouts/passkeys/rails/application.html.erb
373
+ - config/initializers/application_controller.rb
364
374
  - config/routes.rb
365
375
  - db/migrate/20230620012530_create_passkeys_rails_agents.rb
366
376
  - db/migrate/20230620012600_create_passkeys_rails_passkeys.rb
@@ -369,16 +379,7 @@ files:
369
379
  - lib/generators/passkeys_rails/templates/README
370
380
  - lib/generators/passkeys_rails/templates/passkeys_rails_config.rb
371
381
  - lib/passkeys-rails.rb
372
- - lib/passkeys/rails/controllers/helpers.rb
373
382
  - lib/passkeys/rails/engine.rb
374
- - lib/passkeys/rails/interactors/begin_authentication.rb
375
- - lib/passkeys/rails/interactors/begin_challenge.rb
376
- - lib/passkeys/rails/interactors/begin_registration.rb
377
- - lib/passkeys/rails/interactors/finish_authentication.rb
378
- - lib/passkeys/rails/interactors/finish_registration.rb
379
- - lib/passkeys/rails/interactors/generate_auth_token.rb
380
- - lib/passkeys/rails/interactors/refresh_token.rb
381
- - lib/passkeys/rails/interactors/validate_auth_token.rb
382
383
  - lib/passkeys/rails/railtie.rb
383
384
  - lib/passkeys/rails/version.rb
384
385
  - lib/tasks/passkeys_rails_tasks.rake
@@ -405,7 +406,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
405
406
  - !ruby/object:Gem::Version
406
407
  version: '0'
407
408
  requirements: []
408
- rubygems_version: 3.4.12
409
+ rubygems_version: 3.4.17
409
410
  signing_key:
410
411
  specification_version: 4
411
412
  summary: PassKey authentication back end with simple API
@@ -1,33 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,37 +0,0 @@
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
@@ -1,25 +0,0 @@
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
@@ -1,55 +0,0 @@
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
@@ -1,79 +0,0 @@
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
@@ -1,29 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,35 +0,0 @@
1
- module Passkeys
2
- module Rails
3
- class ValidateAuthToken
4
- include Interactor
5
-
6
- delegate :auth_token, to: :context
7
-
8
- def call
9
- context.fail!(code: :missing_token, message: "X-Auth header is required") if auth_token.blank?
10
-
11
- context.agent = fetch_agent
12
- end
13
-
14
- private
15
-
16
- def fetch_agent
17
- agent = Agent.find_by(id: payload['agent_id'])
18
- context.fail!(code: :invalid_token, message: "Invalid token - no agent exists with agent_id") if agent.blank?
19
-
20
- agent
21
- end
22
-
23
- def payload
24
- JWT.decode(auth_token,
25
- Passkeys::Rails.auth_token_secret,
26
- true,
27
- { required_claims: %w[exp agent_id], algorithm: Passkeys::Rails.auth_token_algorithm }).first
28
- rescue JWT::ExpiredSignature
29
- context.fail!(code: :expired_token, message: "The token has expired")
30
- rescue StandardError => e
31
- context.fail!(code: :token_error, message: e.message)
32
- end
33
- end
34
- end
35
- end