devise-passkeys 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|