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
@@ -0,0 +1,35 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'passkeys-rails'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Passkeys
|
5
|
+
module Rails
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
railtie_name :passkeys_rails
|
8
|
+
|
9
|
+
rake_tasks do
|
10
|
+
path = File.expand_path(__dir__)
|
11
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
12
|
+
end
|
13
|
+
|
14
|
+
generators do
|
15
|
+
require "generators/passkeys_rails/install_generator"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# rubocop:disable Naming/FileName
|
2
|
+
require 'passkeys/rails/engine'
|
3
|
+
require 'passkeys/rails/version'
|
4
|
+
require_relative "generators/passkeys_rails/install_generator"
|
5
|
+
|
6
|
+
module Passkeys
|
7
|
+
module Rails
|
8
|
+
# Secret used to encode the auth token.
|
9
|
+
# Rails.application.secret_key_base is used if none is defined here.
|
10
|
+
# Changing this value will invalidate all tokens that have been fetched
|
11
|
+
# through the API.
|
12
|
+
mattr_accessor(:auth_token_secret)
|
13
|
+
|
14
|
+
# Algorithm used to generate the auth token.
|
15
|
+
# Changing this value will invalidate all tokens that have been fetched
|
16
|
+
# through the API.
|
17
|
+
mattr_accessor :auth_token_algorithm, default: "HS256"
|
18
|
+
|
19
|
+
# How long the auth token is valid before requiring a refresh or new login.
|
20
|
+
# Set it to 0 for no expiration (not recommended in production).
|
21
|
+
mattr_accessor :auth_token_expires_in, default: 30.days
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def config
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'passkeys/rails/railtie' if defined?(Rails)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
ActiveSupport.on_load(:before_initialize) do
|
34
|
+
Passkeys::Rails.auth_token_secret ||= Rails.application.secret_key_base
|
35
|
+
end
|
36
|
+
# rubocop:enable Naming/FileName
|
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.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Troy Anderson
|
@@ -338,7 +338,7 @@ dependencies:
|
|
338
338
|
- - "~>"
|
339
339
|
- !ruby/object:Gem::Version
|
340
340
|
version: 2.22.0
|
341
|
-
description: Devise is awesome, but we don't need all that UI/UX for PassKeys.
|
341
|
+
description: Devise is awesome, but we don't need all that UI/UX for PassKeys. This
|
342
342
|
gem is to make it easy to provide a back end that authenticates a mobile front end
|
343
343
|
with PassKeys.
|
344
344
|
email:
|
@@ -347,29 +347,20 @@ executables: []
|
|
347
347
|
extensions: []
|
348
348
|
extra_rdoc_files: []
|
349
349
|
files:
|
350
|
+
- CHANGELOG.md
|
350
351
|
- MIT-LICENSE
|
351
352
|
- README.md
|
352
353
|
- Rakefile
|
353
354
|
- app/assets/config/passkeys_rails_manifest.js
|
354
355
|
- app/assets/stylesheets/passkeys_rails/application.css
|
355
|
-
- app/controllers/
|
356
|
-
- app/controllers/
|
357
|
-
- app/
|
358
|
-
- app/
|
359
|
-
- app/
|
360
|
-
- app/
|
361
|
-
- app/
|
362
|
-
- app/
|
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
|
367
|
-
- app/models/concerns/passkeys_rails/authenticatable.rb
|
368
|
-
- app/models/passkeys_rails/agent.rb
|
369
|
-
- app/models/passkeys_rails/application_record.rb
|
370
|
-
- app/models/passkeys_rails/error.rb
|
371
|
-
- app/models/passkeys_rails/passkey.rb
|
372
|
-
- app/views/layouts/passkeys_rails/application.html.erb
|
356
|
+
- app/controllers/passkeys/rails/application_controller.rb
|
357
|
+
- app/controllers/passkeys/rails/passkeys_controller.rb
|
358
|
+
- app/models/concerns/passkeys/rails/authenticatable.rb
|
359
|
+
- app/models/passkeys/rails/agent.rb
|
360
|
+
- app/models/passkeys/rails/application_record.rb
|
361
|
+
- app/models/passkeys/rails/error.rb
|
362
|
+
- app/models/passkeys/rails/passkey.rb
|
363
|
+
- app/views/layouts/passkeys/rails/application.html.erb
|
373
364
|
- config/routes.rb
|
374
365
|
- db/migrate/20230620012530_create_passkeys_rails_agents.rb
|
375
366
|
- db/migrate/20230620012600_create_passkeys_rails_passkeys.rb
|
@@ -377,11 +368,19 @@ files:
|
|
377
368
|
- lib/generators/passkeys_rails/install_generator.rb
|
378
369
|
- lib/generators/passkeys_rails/templates/README
|
379
370
|
- lib/generators/passkeys_rails/templates/passkeys_rails_config.rb
|
380
|
-
- lib/
|
381
|
-
- lib/
|
382
|
-
- lib/
|
383
|
-
- lib/
|
384
|
-
- lib/
|
371
|
+
- lib/passkeys-rails.rb
|
372
|
+
- lib/passkeys/rails/controllers/helpers.rb
|
373
|
+
- 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
|
+
- lib/passkeys/rails/railtie.rb
|
383
|
+
- lib/passkeys/rails/version.rb
|
385
384
|
- lib/tasks/passkeys_rails_tasks.rake
|
386
385
|
homepage: https://github.com/alliedcode/passkeys-rails
|
387
386
|
licenses:
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
2
|
-
class ApplicationController < ActionController::Base
|
3
|
-
rescue_from ::Interactor::Failure, with: :handle_interactor_failure
|
4
|
-
rescue_from ActionController::ParameterMissing, with: :handle_missing_parameter
|
5
|
-
|
6
|
-
protected
|
7
|
-
|
8
|
-
def handle_missing_parameter(error)
|
9
|
-
render_error(:authentication, 'missing_parameter', error.message)
|
10
|
-
end
|
11
|
-
|
12
|
-
def handle_interactor_failure(failure)
|
13
|
-
render_error(:authentication, failure.context.code, failure.context.message)
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def render_error(context, code, message, status: :unprocessable_entity)
|
19
|
-
render json: { error: { context:, code:, message: } }, status:
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,61 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
2
|
-
class PasskeysController < ApplicationController
|
3
|
-
def challenge
|
4
|
-
result = PasskeysRails::BeginChallenge.call!(username: challenge_params[:username])
|
5
|
-
|
6
|
-
# Store the challenge so we can verify the future register or authentication request
|
7
|
-
session[:passkeys_rails] = result.session_data
|
8
|
-
|
9
|
-
render json: result.response.as_json
|
10
|
-
end
|
11
|
-
|
12
|
-
def register
|
13
|
-
result = PasskeysRails::FinishRegistration.call!(credential: attestation_credential_params.to_h,
|
14
|
-
authenticatable_class:,
|
15
|
-
username: session.dig(:passkeys_rails, :username),
|
16
|
-
challenge: session.dig(:passkeys_rails, :challenge))
|
17
|
-
|
18
|
-
render json: { username: result.username, auth_token: result.auth_token }
|
19
|
-
end
|
20
|
-
|
21
|
-
def authenticate
|
22
|
-
result = PasskeysRails::FinishAuthentication.call!(credential: authentication_params.to_h,
|
23
|
-
challenge: session.dig(:passkeys_rails, :challenge))
|
24
|
-
|
25
|
-
render json: { username: result.username, auth_token: result.auth_token }
|
26
|
-
end
|
27
|
-
|
28
|
-
def refresh
|
29
|
-
result = PasskeysRails::RefreshToken.call!(token: refresh_params[:auth_token])
|
30
|
-
render json: { username: result.username, auth_token: result.auth_token }
|
31
|
-
end
|
32
|
-
|
33
|
-
protected
|
34
|
-
|
35
|
-
def challenge_params
|
36
|
-
params.permit(:username)
|
37
|
-
end
|
38
|
-
|
39
|
-
def attestation_credential_params
|
40
|
-
credential = params.require(:credential)
|
41
|
-
credential.require(%i[id rawId type response])
|
42
|
-
credential.require(:response).require(%i[attestationObject clientDataJSON])
|
43
|
-
credential.permit(:id, :rawId, :type, { response: %i[attestationObject clientDataJSON] })
|
44
|
-
end
|
45
|
-
|
46
|
-
def authenticatable_class
|
47
|
-
params[:authenticatable_class]
|
48
|
-
end
|
49
|
-
|
50
|
-
def authentication_params
|
51
|
-
params.require(%i[id rawId type response])
|
52
|
-
params.require(:response).require(%i[authenticatorData clientDataJSON signature userHandle])
|
53
|
-
params.permit(:id, :rawId, :type, { response: %i[authenticatorData clientDataJSON signature userHandle] })
|
54
|
-
end
|
55
|
-
|
56
|
-
def refresh_params
|
57
|
-
params.require(:auth_token)
|
58
|
-
params.permit(:auth_token)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
2
|
-
module ApplicationHelper
|
3
|
-
def current_agent
|
4
|
-
return nil if request.headers['HTTP_X_AUTH'].blank?
|
5
|
-
|
6
|
-
@current_agent ||= validated_auth_token&.success? && validated_auth_token&.agent
|
7
|
-
end
|
8
|
-
|
9
|
-
def authenticate!
|
10
|
-
return if validated_auth_token.success?
|
11
|
-
|
12
|
-
raise PasskeysRails::Error.new(:authentication,
|
13
|
-
code: :unauthorized,
|
14
|
-
message: "You are not authorized to access this resource.")
|
15
|
-
end
|
16
|
-
|
17
|
-
def validated_auth_token
|
18
|
-
@validated_auth_token ||= ValidateAuthToken.call(auth_token: request.headers['HTTP_X_AUTH'])
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
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
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
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
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# Finish authentication ceremony
|
2
|
-
module PasskeysRails
|
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
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# Finish registration ceremony
|
2
|
-
module PasskeysRails
|
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
|
@@ -1,27 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
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
|
-
PasskeysRails.auth_token_secret,
|
16
|
-
PasskeysRails.auth_token_algorithm)
|
17
|
-
end
|
18
|
-
|
19
|
-
def jwt_payload
|
20
|
-
expiration = (Time.current + PasskeysRails.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
|
@@ -1,17 +0,0 @@
|
|
1
|
-
# Finish authentication ceremony
|
2
|
-
module PasskeysRails
|
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
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
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
|
-
PasskeysRails.auth_token_secret,
|
25
|
-
true,
|
26
|
-
{ required_claims: %w[exp agent_id], algorithm: PasskeysRails.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,17 +0,0 @@
|
|
1
|
-
require 'active_support/concern'
|
2
|
-
|
3
|
-
module PasskeysRails
|
4
|
-
module Authenticatable
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do
|
8
|
-
has_one :agent, as: :authenticatable
|
9
|
-
|
10
|
-
delegate :registered?, to: :agent, allow_nil: true
|
11
|
-
|
12
|
-
def registering_with(_agent)
|
13
|
-
# initialize required attributes
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
module PasskeysRails
|
2
|
-
class Agent < ApplicationRecord
|
3
|
-
belongs_to :authenticatable, polymorphic: true, optional: true
|
4
|
-
has_many :passkeys
|
5
|
-
|
6
|
-
scope :registered, -> { where.not registered_at: nil }
|
7
|
-
scope :unregistered, -> { where registered_at: nil }
|
8
|
-
|
9
|
-
validates :username, presence: true, uniqueness: true
|
10
|
-
|
11
|
-
def registered?
|
12
|
-
registered_at.present?
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
require_relative 'error_middleware'
|
2
|
-
module PasskeysRails
|
3
|
-
class Engine < ::Rails::Engine
|
4
|
-
isolate_namespace PasskeysRails
|
5
|
-
|
6
|
-
config.generators do |g|
|
7
|
-
g.test_framework :rspec
|
8
|
-
g.fixture_replacement :factory_bot
|
9
|
-
g.factory_bot dir: 'spec/factories'
|
10
|
-
g.assets false
|
11
|
-
g.helper false
|
12
|
-
end
|
13
|
-
|
14
|
-
config.to_prepare do
|
15
|
-
# include our helper methods in the host application's ApplicationController
|
16
|
-
::ApplicationController.include ApplicationHelper
|
17
|
-
end
|
18
|
-
|
19
|
-
# provide a way to bail out of the render flow if needed
|
20
|
-
initializer 'passkeys_rails.configure.middleware' do |app|
|
21
|
-
app.middleware.use ErrorMiddleware
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|