passkeys-rails 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +17 -2
- data/app/controllers/passkeys/rails/application_controller.rb +24 -0
- data/app/controllers/passkeys/rails/passkeys_controller.rb +63 -0
- data/app/models/concerns/passkeys/rails/authenticatable.rb +19 -0
- data/app/models/passkeys/rails/agent.rb +17 -0
- data/app/models/passkeys/rails/application_record.rb +7 -0
- data/app/models/passkeys/rails/error.rb +16 -0
- data/app/models/passkeys/rails/passkey.rb +10 -0
- data/config/routes.rb +1 -1
- data/lib/generators/passkeys_rails/USAGE +1 -1
- data/lib/generators/passkeys_rails/install_generator.rb +14 -12
- data/lib/generators/passkeys_rails/templates/README +2 -2
- data/lib/generators/passkeys_rails/templates/passkeys_rails_config.rb +2 -2
- data/lib/passkeys/rails/controllers/helpers.rb +33 -0
- data/lib/passkeys/rails/engine.rb +44 -0
- data/lib/passkeys/rails/interactors/begin_authentication.rb +11 -0
- data/lib/passkeys/rails/interactors/begin_challenge.rb +37 -0
- data/lib/passkeys/rails/interactors/begin_registration.rb +25 -0
- data/lib/passkeys/rails/interactors/finish_authentication.rb +55 -0
- data/lib/passkeys/rails/interactors/finish_registration.rb +79 -0
- data/lib/passkeys/rails/interactors/generate_auth_token.rb +29 -0
- data/lib/passkeys/rails/interactors/refresh_token.rb +19 -0
- data/lib/passkeys/rails/interactors/validate_auth_token.rb +35 -0
- data/lib/passkeys/rails/railtie.rb +19 -0
- data/lib/passkeys/rails/version.rb +5 -0
- data/lib/passkeys-rails.rb +36 -0
- metadata +24 -25
- data/app/controllers/passkeys_rails/application_controller.rb +0 -22
- data/app/controllers/passkeys_rails/passkeys_controller.rb +0 -61
- data/app/helpers/passkeys_rails/application_helper.rb +0 -21
- data/app/helpers/passkeys_rails/passkeys_helper.rb +0 -4
- data/app/interactors/passkeys_rails/begin_authentication.rb +0 -9
- data/app/interactors/passkeys_rails/begin_challenge.rb +0 -35
- data/app/interactors/passkeys_rails/begin_registration.rb +0 -23
- data/app/interactors/passkeys_rails/finish_authentication.rb +0 -53
- data/app/interactors/passkeys_rails/finish_registration.rb +0 -77
- data/app/interactors/passkeys_rails/generate_auth_token.rb +0 -27
- data/app/interactors/passkeys_rails/refresh_token.rb +0 -17
- data/app/interactors/passkeys_rails/validate_auth_token.rb +0 -33
- data/app/models/concerns/passkeys_rails/authenticatable.rb +0 -17
- data/app/models/passkeys_rails/agent.rb +0 -15
- data/app/models/passkeys_rails/application_record.rb +0 -5
- data/app/models/passkeys_rails/error.rb +0 -14
- data/app/models/passkeys_rails/passkey.rb +0 -8
- data/lib/passkeys_rails/engine.rb +0 -24
- data/lib/passkeys_rails/error_middleware.rb +0 -17
- data/lib/passkeys_rails/railtie.rb +0 -17
- data/lib/passkeys_rails/version.rb +0 -3
- data/lib/passkeys_rails.rb +0 -38
- /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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6b2553d498c30365341aba0aad49eafd09c5b49dd5453bbad29c20a026ab39c
|
4
|
+
data.tar.gz: 795d362e15d977c47b5381a4fe5cc852cdd546a0fa8f81e5a5452343f2b4fa1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15694fd664f6b60343ede055acfefdfef0d8378ffe386420e7cd5d996920fe19a9321ac1148f3e22605b85a8486cbd1f56999167a84be33f9acf950ea0e5e722
|
7
|
+
data.tar.gz: b6f224af15ee0feb2489c786eefe68293d4854b43b7dcf3a2d17dea4514497cdd5df5f80bd5941972918b4c05864529c62bbe694c09ff58540a87529216bf8f3
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
[](https://badge.fury.io/rb/passkeys-rails)
|
1
2
|
[](https://travis-ci.org/alliedcode/passkeys-rails)
|
2
3
|
[](https://codecov.io/gh/alliedcode/passkeys-rails)
|
3
4
|
|
4
|
-
#
|
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
|
-
|
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
|
data/config/routes.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
require 'rails/generators'
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
9
|
+
def copy_config
|
10
|
+
template 'passkeys_rails_config.rb', "config/initializers/passkeys_rails.rb"
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def add_routes
|
14
|
+
route 'mount Passkeys::Rails::Engine => "/passkeys_rails"'
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
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 :
|
9
|
+
before_action :authenticate_passkey!, except: [:index]
|
10
10
|
|
11
|
-
2. Optionally include
|
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
|
@@ -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,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
|