devise-passkeys 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +59 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +151 -0
- data/LICENSE.txt +21 -0
- data/README.md +200 -0
- data/Rakefile +17 -0
- data/devise-passkeys.gemspec +41 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_6.gemfile +25 -0
- data/gemfiles/rails_7.gemfile +25 -0
- data/lib/devise/passkeys/controllers/concerns/passkey_reauthentication.rb +39 -0
- data/lib/devise/passkeys/controllers/concerns/reauthentication_challenge.rb +21 -0
- data/lib/devise/passkeys/controllers/passkeys_controller_concern.rb +140 -0
- data/lib/devise/passkeys/controllers/reauthentication_controller_concern.rb +78 -0
- data/lib/devise/passkeys/controllers/registrations_controller_concern.rb +141 -0
- data/lib/devise/passkeys/controllers/sessions_controller_concern.rb +38 -0
- data/lib/devise/passkeys/controllers.rb +15 -0
- data/lib/devise/passkeys/model.rb +9 -0
- data/lib/devise/passkeys/passkey_issuer.rb +51 -0
- data/lib/devise/passkeys/rails.rb +6 -0
- data/lib/devise/passkeys/reauthentication_strategy.rb +16 -0
- data/lib/devise/passkeys/strategy.rb +64 -0
- data/lib/devise/passkeys/version.rb +7 -0
- data/lib/devise/passkeys.rb +29 -0
- data/sig/devise/passkeys.rbs +6 -0
- metadata +105 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module Concerns
|
7
|
+
module PasskeyReauthentication
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
def store_reauthentication_token_in_session
|
11
|
+
session[passkey_reauthentication_token_key] = Devise.friendly_token(50)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stored_reauthentication_token
|
15
|
+
session[passkey_reauthentication_token_key]
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear_reauthentication_token!
|
19
|
+
session.delete(passkey_reauthentication_token_key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def consume_reauthentication_token!
|
23
|
+
value = stored_reauthentication_token
|
24
|
+
clear_reauthentication_token!
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_reauthentication_token?(given_reauthentication_token:)
|
29
|
+
Devise.secure_compare(consume_reauthentication_token!, given_reauthentication_token)
|
30
|
+
end
|
31
|
+
|
32
|
+
def passkey_reauthentication_token_key
|
33
|
+
"#{resource_name}_current_reauthentication_token"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module Concerns
|
7
|
+
module ReauthenticationChallenge
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
def passkey_reauthentication_challenge_session_key
|
11
|
+
"#{resource_name}_current_reauthentication_challenge"
|
12
|
+
end
|
13
|
+
|
14
|
+
def store_reauthentication_challenge_in_session(options_for_authentication:)
|
15
|
+
session[passkey_reauthentication_challenge_session_key] = options_for_authentication.challenge
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module PasskeysControllerConcern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Devise::Passkeys::Controllers::Concerns::PasskeyReauthentication
|
11
|
+
include Devise::Passkeys::Controllers::Concerns::ReauthenticationChallenge
|
12
|
+
include Warden::WebAuthn::AuthenticationInitiationHelpers
|
13
|
+
include Warden::WebAuthn::RegistrationHelpers
|
14
|
+
include Warden::WebAuthn::StrategyHelpers
|
15
|
+
|
16
|
+
prepend_before_action :authenticate_scope!
|
17
|
+
before_action :ensure_at_least_one_passkey, only: %i[new_destroy_challenge destroy]
|
18
|
+
before_action :find_passkey, only: %i[new_destroy_challenge destroy]
|
19
|
+
|
20
|
+
before_action :verify_passkey_challenge, only: [:create]
|
21
|
+
before_action :verify_reauthentication_token, only: %i[create destroy]
|
22
|
+
|
23
|
+
# Authenticates the current scope and gets the current resource from the session.
|
24
|
+
def authenticate_scope!
|
25
|
+
send(:"authenticate_#{resource_name}!", force: true)
|
26
|
+
self.resource = send(:"current_#{resource_name}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def registration_challenge_key
|
30
|
+
"#{resource_name}_passkey_creation_challenge"
|
31
|
+
end
|
32
|
+
|
33
|
+
def errors
|
34
|
+
warden.errors
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_credential
|
38
|
+
passkey_params[:credential]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def new_create_challenge
|
43
|
+
options_for_registration = generate_registration_options(
|
44
|
+
relying_party: relying_party,
|
45
|
+
user_details: user_details_for_registration,
|
46
|
+
exclude: exclude_external_ids_for_registration
|
47
|
+
)
|
48
|
+
|
49
|
+
store_challenge_in_session(options_for_registration: options_for_registration)
|
50
|
+
|
51
|
+
render json: options_for_registration
|
52
|
+
end
|
53
|
+
|
54
|
+
def create
|
55
|
+
create_passkey(resource: resource)
|
56
|
+
end
|
57
|
+
|
58
|
+
def new_destroy_challenge
|
59
|
+
allowed_passkeys = (resource.passkeys - [@passkey])
|
60
|
+
|
61
|
+
options_for_authentication = generate_authentication_options(relying_party: relying_party,
|
62
|
+
options: { allow: allowed_passkeys.pluck(:external_id) })
|
63
|
+
|
64
|
+
store_reauthentication_challenge_in_session(options_for_authentication: options_for_authentication)
|
65
|
+
|
66
|
+
render json: options_for_authentication
|
67
|
+
end
|
68
|
+
|
69
|
+
def destroy
|
70
|
+
@passkey.destroy
|
71
|
+
redirect_to root_path
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def create_passkey(resource:)
|
77
|
+
passkey = resource.passkeys.create!(
|
78
|
+
label: passkey_params[:label],
|
79
|
+
public_key: @webauthn_credential.public_key,
|
80
|
+
external_id: Base64.strict_encode64(@webauthn_credential.raw_id),
|
81
|
+
sign_count: @webauthn_credential.sign_count,
|
82
|
+
last_used_at: nil
|
83
|
+
)
|
84
|
+
yield [resource, passkey] if block_given?
|
85
|
+
redirect_to root_path
|
86
|
+
end
|
87
|
+
|
88
|
+
def exclude_external_ids_for_registration
|
89
|
+
resource.passkeys.pluck(:external_id)
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_details_for_registration
|
93
|
+
{ id: resource.webauthn_id, name: resource.email }
|
94
|
+
end
|
95
|
+
|
96
|
+
def verify_passkey_challenge
|
97
|
+
if parsed_credential.nil?
|
98
|
+
render json: { message: find_message(:credential_missing_or_could_not_be_parsed) }, status: :bad_request
|
99
|
+
delete_registration_challenge
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
begin
|
103
|
+
@webauthn_credential = verify_registration(relying_party: relying_party)
|
104
|
+
rescue ::WebAuthn::Error => e
|
105
|
+
error_key = Warden::WebAuthn::ErrorKeyFinder.webauthn_error_key(exception: e)
|
106
|
+
render json: { message: find_message(error_key) }, status: :bad_request
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def passkey_params
|
111
|
+
params.require(:passkey).permit(:label, :credential)
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_at_least_one_passkey
|
115
|
+
return unless current_user.passkeys.count <= 1
|
116
|
+
|
117
|
+
render json: { error: find_message(:must_be_at_least_one_passkey) }, status: :bad_request
|
118
|
+
end
|
119
|
+
|
120
|
+
def find_passkey
|
121
|
+
@passkey = resource.passkeys.where(id: params[:id]).first
|
122
|
+
return unless @passkey.nil?
|
123
|
+
|
124
|
+
head :not_found
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def verify_reauthentication_token
|
129
|
+
return if valid_reauthentication_token?(given_reauthentication_token: reauthentication_params[:reauthentication_token])
|
130
|
+
|
131
|
+
render json: { error: find_message(:not_reauthenticated) }, status: :bad_request
|
132
|
+
end
|
133
|
+
|
134
|
+
def reauthentication_params
|
135
|
+
params.require(:passkey).permit(:reauthentication_token)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module ReauthenticationControllerConcern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Devise::Passkeys::Controllers::Concerns::PasskeyReauthentication
|
11
|
+
include Devise::Passkeys::Controllers::Concerns::ReauthenticationChallenge
|
12
|
+
include Warden::WebAuthn::AuthenticationInitiationHelpers
|
13
|
+
include Warden::WebAuthn::StrategyHelpers
|
14
|
+
|
15
|
+
prepend_before_action :authenticate_scope!
|
16
|
+
|
17
|
+
before_action :prepare_params, only: [:reauthenticate]
|
18
|
+
|
19
|
+
# Prepending is crucial to ensure that the relying party is set in the
|
20
|
+
# request.env before the strategy is executed
|
21
|
+
prepend_before_action :set_relying_party_in_request_env
|
22
|
+
|
23
|
+
# Authenticates the current scope and gets the current resource from the session.
|
24
|
+
def authenticate_scope!
|
25
|
+
send(:"authenticate_#{resource_name}!", force: true)
|
26
|
+
self.resource = send(:"current_#{resource_name}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def new_challenge
|
31
|
+
options_for_authentication = generate_authentication_options(relying_party: relying_party,
|
32
|
+
options: { allow: resource.passkeys.pluck(:external_id) })
|
33
|
+
|
34
|
+
store_reauthentication_challenge_in_session(options_for_authentication: options_for_authentication)
|
35
|
+
|
36
|
+
render json: options_for_authentication
|
37
|
+
end
|
38
|
+
|
39
|
+
def reauthenticate
|
40
|
+
sign_out(resource)
|
41
|
+
self.resource = warden.authenticate!(strategy, auth_options)
|
42
|
+
sign_in(resource, event: :passkey_reauthentication)
|
43
|
+
yield resource if block_given?
|
44
|
+
|
45
|
+
store_reauthentication_token_in_session
|
46
|
+
|
47
|
+
render json: { reauthentication_token: stored_reauthentication_token }
|
48
|
+
ensure
|
49
|
+
delete_reauthentication_challenge
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def prepare_params
|
55
|
+
request.params[resource_name] = ActionController::Parameters.new({
|
56
|
+
passkey_credential: params[:passkey_credential]
|
57
|
+
})
|
58
|
+
end
|
59
|
+
|
60
|
+
def strategy
|
61
|
+
:passkey_reauthentication
|
62
|
+
end
|
63
|
+
|
64
|
+
def auth_options
|
65
|
+
{ scope: resource_name, recall: root_path }
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete_reauthentication_challenge
|
69
|
+
session.delete(passkey_reauthentication_challenge_session_key)
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_relying_party_in_request_env
|
73
|
+
raise "need to define relying_party for this SessionsController"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module RegistrationsControllerConcern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Devise::Passkeys::Controllers::Concerns::PasskeyReauthentication
|
11
|
+
include Warden::WebAuthn::RegistrationHelpers
|
12
|
+
|
13
|
+
before_action :require_no_authentication, only: [:new_challenge]
|
14
|
+
before_action :require_email_and_passkey_label, only: %i[new_challenge create]
|
15
|
+
before_action :verify_passkey_registration_challenge, only: [:create]
|
16
|
+
before_action :configure_sign_up_params, only: [:create]
|
17
|
+
|
18
|
+
before_action :verify_reauthentication_token, only: %i[update destroy]
|
19
|
+
|
20
|
+
def registration_user_id_key
|
21
|
+
"#{resource_name}_current_webauthn_user_id"
|
22
|
+
end
|
23
|
+
|
24
|
+
def registration_challenge_key
|
25
|
+
"#{resource_name}_current_webauthn_registration_challenge"
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw_credential
|
29
|
+
passkey_params[:passkey_credential]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def new_challenge
|
34
|
+
options_for_registration = generate_registration_options(
|
35
|
+
relying_party: relying_party,
|
36
|
+
user_details: user_details_for_registration,
|
37
|
+
exclude: exclude_external_ids_for_registration
|
38
|
+
)
|
39
|
+
|
40
|
+
store_challenge_in_session(options_for_registration: options_for_registration)
|
41
|
+
|
42
|
+
render json: options_for_registration
|
43
|
+
end
|
44
|
+
|
45
|
+
def create
|
46
|
+
super do |resource|
|
47
|
+
create_resource_and_passkey(resource: resource)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def create_resource_and_passkey(resource:)
|
54
|
+
return unless resource.persisted?
|
55
|
+
|
56
|
+
passkey = create_passkey(resource: resource)
|
57
|
+
|
58
|
+
yield [resource, passkey] if block_given?
|
59
|
+
delete_registration_user_id!
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_passkey(resource:)
|
63
|
+
resource.passkeys.create!(
|
64
|
+
label: passkey_params[:passkey_label],
|
65
|
+
public_key: @webauthn_credential.public_key,
|
66
|
+
external_id: Base64.strict_encode64(@webauthn_credential.raw_id),
|
67
|
+
sign_count: @webauthn_credential.sign_count,
|
68
|
+
last_used_at: Time.now.utc
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def verify_reauthentication_token
|
73
|
+
return if valid_reauthentication_token?(given_reauthentication_token: reauthentication_params[:reauthentication_token])
|
74
|
+
|
75
|
+
render json: { error: find_message(:not_reauthenticated) }, status: :bad_request
|
76
|
+
end
|
77
|
+
|
78
|
+
def reauthentication_params
|
79
|
+
params.require(:user).permit(:reauthentication_token)
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_resource(resource, params)
|
83
|
+
resource.update(params)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Override if you need to exclude certain external IDs
|
87
|
+
def exclude_external_ids_for_registration
|
88
|
+
[]
|
89
|
+
end
|
90
|
+
|
91
|
+
def passkey_params
|
92
|
+
params.require(resource_name).permit(:passkey_label, :passkey_credential)
|
93
|
+
end
|
94
|
+
|
95
|
+
def require_email_and_passkey_label
|
96
|
+
if sign_up_params[:email].blank?
|
97
|
+
render json: { message: find_message(:email_missing) }, status: :bad_request
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
|
101
|
+
if passkey_params[:passkey_label].blank?
|
102
|
+
render json: { message: find_message(:passkey_label_missing) }, status: :bad_request
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_passkey_registration_challenge
|
110
|
+
@webauthn_credential = verify_registration(relying_party: relying_party)
|
111
|
+
rescue ::WebAuthn::Error => e
|
112
|
+
error_key = Warden::WebAuthn::ErrorKeyFinder.webauthn_error_key(exception: e)
|
113
|
+
render json: { message: find_message(error_key) }, status: :bad_request
|
114
|
+
end
|
115
|
+
|
116
|
+
# If you have extra params to permit, append them to the sanitizer.
|
117
|
+
def configure_sign_up_params
|
118
|
+
params[:user][:webauthn_id] = registration_user_id
|
119
|
+
devise_parameter_sanitizer.permit(:sign_up, keys: [:webauthn_id])
|
120
|
+
end
|
121
|
+
|
122
|
+
def user_details_for_registration
|
123
|
+
store_registration_user_id
|
124
|
+
{ id: registration_user_id, name: sign_up_params[:email] }
|
125
|
+
end
|
126
|
+
|
127
|
+
def registration_user_id
|
128
|
+
session[registration_user_id_key]
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete_registration_user_id!
|
132
|
+
session.delete(registration_user_id_key)
|
133
|
+
end
|
134
|
+
|
135
|
+
def store_registration_user_id
|
136
|
+
session[registration_user_id_key] = WebAuthn.generate_user_id
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
module Controllers
|
6
|
+
module SessionsControllerConcern
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Warden::WebAuthn::AuthenticationInitiationHelpers
|
11
|
+
include Warden::WebAuthn::StrategyHelpers
|
12
|
+
|
13
|
+
# Prepending is crucial to ensure that the relying party is set in the
|
14
|
+
# request.env before the strategy is executed
|
15
|
+
prepend_before_action :set_relying_party_in_request_env
|
16
|
+
|
17
|
+
def authentication_challenge_key
|
18
|
+
"#{resource_name}_current_webauthn_authentication_challenge"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def new_challenge
|
23
|
+
options_for_authentication = generate_authentication_options(relying_party: relying_party)
|
24
|
+
|
25
|
+
store_challenge_in_session(options_for_authentication: options_for_authentication)
|
26
|
+
|
27
|
+
render json: options_for_authentication
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def set_relying_party_in_request_env
|
33
|
+
raise "need to define relying_party for this SessionsController"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "controllers/concerns/passkey_reauthentication"
|
4
|
+
require_relative "controllers/concerns/reauthentication_challenge"
|
5
|
+
require_relative "controllers/sessions_controller_concern"
|
6
|
+
require_relative "controllers/registrations_controller_concern"
|
7
|
+
require_relative "controllers/reauthentication_controller_concern"
|
8
|
+
require_relative "controllers/passkeys_controller_concern"
|
9
|
+
|
10
|
+
module Devise
|
11
|
+
module Passkeys
|
12
|
+
module Controllers
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Devise
|
4
|
+
module Passkeys
|
5
|
+
class PasskeyIssuer
|
6
|
+
def self.build
|
7
|
+
new
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_and_return_passkey(resource:, label:, webauthn_credential:, extra_attributes: {})
|
11
|
+
# rubocop:disable Lint/UselessAssignment
|
12
|
+
passkey_class = passkey_class(resource)
|
13
|
+
# rubocop:enable Lint/UselessAssignment
|
14
|
+
|
15
|
+
resource.passkeys.create!({
|
16
|
+
label: label,
|
17
|
+
public_key: webauthn_credential.public_key,
|
18
|
+
external_id: Base64.strict_encode64(webauthn_credential.raw_id),
|
19
|
+
sign_count: webauthn_credential.sign_count,
|
20
|
+
last_used_at: nil
|
21
|
+
}.merge(extra_attributes))
|
22
|
+
end
|
23
|
+
|
24
|
+
class CredentialFinder
|
25
|
+
attr_reader :resource_class
|
26
|
+
|
27
|
+
def initialize(resource_class:)
|
28
|
+
@resource_class = resource_class
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_with_credential_id(encoded_credential_id)
|
32
|
+
resource_class.passkeys_class.where(external_id: encoded_credential_id).first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_accessor :maximum_passkeys_per_user
|
39
|
+
|
40
|
+
def passkey_class(resource)
|
41
|
+
if resource.respond_to?(:association) # ActiveRecord
|
42
|
+
resource.association(:passkeys).klass
|
43
|
+
elsif resource.respond_to?(:relations) # Mongoid
|
44
|
+
resource.relations["passkeys"].klass
|
45
|
+
else
|
46
|
+
raise "Cannot determine passkey class, unsupported ORM/ODM?"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "devise/strategies/authenticatable"
|
4
|
+
require_relative "passkey_issuer"
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
module Strategies
|
8
|
+
class PasskeyReauthentication < PasskeyAuthenticatable
|
9
|
+
def authentication_challenge_key
|
10
|
+
"#{mapping.singular}_current_reauthentication_challenge"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Warden::Strategies.add(:passkey_reauthentication, Devise::Strategies::PasskeyReauthentication)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "devise/strategies/authenticatable"
|
4
|
+
require_relative "passkey_issuer"
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
module Strategies
|
8
|
+
class PasskeyAuthenticatable < Authenticatable
|
9
|
+
include Warden::WebAuthn::StrategyHelpers
|
10
|
+
|
11
|
+
def store?
|
12
|
+
super && !mapping.to.skip_session_storage.include?(:passkey_auth)
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
return true unless parsed_credential.nil?
|
17
|
+
|
18
|
+
# rubocop:disable Lint/UnreachableCode
|
19
|
+
fail(:credential_missing_or_could_not_be_parsed)
|
20
|
+
false
|
21
|
+
# rubocop:enable Lint/UnreachableCode
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticate!
|
25
|
+
passkey = verify_authentication_and_find_stored_credential
|
26
|
+
|
27
|
+
return if passkey.nil?
|
28
|
+
|
29
|
+
resource = mapping.to.find_for_passkey(passkey)
|
30
|
+
|
31
|
+
return fail(:invalid_passkey) unless resource
|
32
|
+
|
33
|
+
if validate(resource)
|
34
|
+
remember_me(resource)
|
35
|
+
resource.after_passkey_authentication
|
36
|
+
record_passkey_use(passkey: passkey)
|
37
|
+
success!(resource)
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
# In paranoid mode, fail with a generic invalid error
|
42
|
+
Devise.paranoid ? fail(:invalid_passkey) : fail(:not_found_in_database)
|
43
|
+
end
|
44
|
+
|
45
|
+
def credential_finder
|
46
|
+
Devise::Passkeys::PasskeyIssuer::CredentialFinder.new(resource_class: mapping.to)
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_credential
|
50
|
+
params.dig(mapping.singular, :passkey_credential)
|
51
|
+
end
|
52
|
+
|
53
|
+
def authentication_challenge_key
|
54
|
+
"#{mapping.singular}_current_webauthn_authentication_challenge"
|
55
|
+
end
|
56
|
+
|
57
|
+
def record_passkey_use(passkey:)
|
58
|
+
passkey.update_attribute(:last_used_at, Time.current)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Warden::Strategies.add(:passkey_authenticatable, Devise::Strategies::PasskeyAuthenticatable)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "devise"
|
4
|
+
require "warden/webauthn"
|
5
|
+
require_relative "passkeys/rails"
|
6
|
+
require_relative "passkeys/model"
|
7
|
+
require_relative "passkeys/controllers"
|
8
|
+
require_relative "passkeys/passkey_issuer"
|
9
|
+
require_relative "passkeys/strategy"
|
10
|
+
require_relative "passkeys/reauthentication_strategy"
|
11
|
+
require_relative "passkeys/version"
|
12
|
+
|
13
|
+
module Devise
|
14
|
+
module Passkeys
|
15
|
+
def self.create_and_return_passkey(resource:, label:, webauthn_credential:, extra_attributes: {})
|
16
|
+
PasskeyIssuer.build.create_and_return_passkey(
|
17
|
+
resource: resource,
|
18
|
+
label: label,
|
19
|
+
webauthn_credential: webauthn_credential,
|
20
|
+
extra_attributes: extra_attributes
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Devise.add_module :passkey_authenticatable,
|
27
|
+
model: "devise/passkeys/model",
|
28
|
+
strategy: true,
|
29
|
+
no_input: true
|