two_factor_auth 0.1.1
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/LICENSE +634 -0
- data/Rakefile +34 -0
- data/app/assets/javascripts/two_factor_auth/application.js +13 -0
- data/app/assets/stylesheets/two_factor_auth/application.css +15 -0
- data/app/controllers/two_factor_auth/authentications_controller.rb +32 -0
- data/app/controllers/two_factor_auth/registrations_controller.rb +29 -0
- data/app/controllers/two_factor_auth/trusted_facets_controller.rb +10 -0
- data/app/controllers/two_factor_auth/two_factor_auth_controller.rb +19 -0
- data/app/helpers/two_factor_auth/application_helper.rb +21 -0
- data/app/helpers/two_factor_auth/authentications_helper.rb +17 -0
- data/app/helpers/two_factor_auth/registrations_helper.rb +17 -0
- data/app/models/two_factor_auth/authentication_client_data.rb +11 -0
- data/app/models/two_factor_auth/authentication_request.rb +30 -0
- data/app/models/two_factor_auth/authentication_response.rb +49 -0
- data/app/models/two_factor_auth/authentication_verifier.rb +68 -0
- data/app/models/two_factor_auth/client_data.rb +57 -0
- data/app/models/two_factor_auth/registration.rb +18 -0
- data/app/models/two_factor_auth/registration_request.rb +33 -0
- data/app/models/two_factor_auth/registration_response.rb +91 -0
- data/app/models/two_factor_auth/registration_verifier.rb +91 -0
- data/app/views/layouts/two_factor_auth/application.html.erb +16 -0
- data/app/views/two_factor_auth/authentications/new.html.erb +30 -0
- data/app/views/two_factor_auth/registrations/new.html.erb +26 -0
- data/config/routes.rb +3 -0
- data/lib/generators/templates/README +6 -0
- data/lib/generators/templates/initializer.rb +38 -0
- data/lib/generators/templates/migration.rb +15 -0
- data/lib/generators/two_factor_auth/install_generator.rb +32 -0
- data/lib/tasks/two_factor_auth_tasks.rake +13 -0
- data/lib/two_factor_auth/authentication_hook.rb +18 -0
- data/lib/two_factor_auth/engine.rb +5 -0
- data/lib/two_factor_auth/registration_hook.rb +17 -0
- data/lib/two_factor_auth/version.rb +3 -0
- data/lib/two_factor_auth.rb +155 -0
- data/test/controllers/two_factor_auth/authentications_controller_test.rb +70 -0
- data/test/controllers/two_factor_auth/registrations_controller_test.rb +57 -0
- data/test/controllers/two_factor_auth/trusted_facets_controller_test.rb +17 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/secrets_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/user.rb +8 -0
- data/test/dummy/app/views/layouts/application.html.erb +16 -0
- data/test/dummy/app/views/secrets/index.html.erb +10 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config/application.rb +24 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/devise.rb +259 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/two_factor_auth.rb +38 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/devise.en.yml +60 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20141026231953_devise_create_users.rb +42 -0
- data/test/dummy/db/migrate/20141224135949_create_two_factor_auth_registrations.rb +15 -0
- data/test/dummy/db/schema.rb +50 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +198 -0
- data/test/dummy/log/test.log +3490 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/dummy/tmp/cache/assets/test/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/test/helpers/two_factor_auth/authentication_helper_test.rb +54 -0
- data/test/helpers/two_factor_auth/registrations_helper_test.rb +34 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/lib/two_factor_auth_test.rb +169 -0
- data/test/models/two_factor_auth/authentication_request_test.rb +35 -0
- data/test/models/two_factor_auth/authentication_response_test.rb +44 -0
- data/test/models/two_factor_auth/authentication_verifier_test.rb +83 -0
- data/test/models/two_factor_auth/client_data_test.rb +79 -0
- data/test/models/two_factor_auth/registration_request_test.rb +29 -0
- data/test/models/two_factor_auth/registration_response_test.rb +87 -0
- data/test/models/two_factor_auth/registration_verifier_test.rb +96 -0
- data/test/test_helper.rb +43 -0
- metadata +351 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
|
2
|
+
// listed below.
|
|
3
|
+
//
|
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
|
6
|
+
//
|
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
|
8
|
+
// compiled file.
|
|
9
|
+
//
|
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
|
11
|
+
// about supported directives.
|
|
12
|
+
//
|
|
13
|
+
//= require_tree .
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any styles
|
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
|
11
|
+
* file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
class AuthenticationsController < TwoFactorAuthController
|
|
3
|
+
skip_before_action :two_factor_auth_authentication
|
|
4
|
+
|
|
5
|
+
include TwoFactorAuth::AuthenticationsHelper
|
|
6
|
+
|
|
7
|
+
def new
|
|
8
|
+
redirect_to after_sign_in_path_for(current_user) if user_two_factor_auth_authenticated?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
keyHandle = TwoFactorAuth::websafe_base64_decode(params.fetch(:keyHandle, ''))
|
|
13
|
+
registration = Registration.find_by_key_handle(keyHandle)
|
|
14
|
+
|
|
15
|
+
verifier = AuthenticationVerifier.new({
|
|
16
|
+
registration: registration,
|
|
17
|
+
request: authentication_request,
|
|
18
|
+
client_data: ClientData.new(encoded: params[:clientData], correct_typ: 'navigator.id.getAssertion'),
|
|
19
|
+
response: AuthenticationResponse.new(encoded: params[:signatureData]),
|
|
20
|
+
})
|
|
21
|
+
clear_pending_challenge
|
|
22
|
+
|
|
23
|
+
if verifier.valid?
|
|
24
|
+
user_two_factor_auth_authenticated! verifier.counter
|
|
25
|
+
redirect_to after_sign_in_path_for(current_user)
|
|
26
|
+
else
|
|
27
|
+
flash[:alert] = "Unable to authenticate"
|
|
28
|
+
render :new, status: 406
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
class RegistrationsController < TwoFactorAuthController
|
|
3
|
+
skip_before_action :two_factor_auth_registration
|
|
4
|
+
skip_before_action :two_factor_auth_authentication
|
|
5
|
+
|
|
6
|
+
include TwoFactorAuth::RegistrationsHelper
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
verifier = RegistrationVerifier.new({
|
|
13
|
+
login: current_user,
|
|
14
|
+
request: registration_request,
|
|
15
|
+
client_data: ClientData.new(encoded: params[:clientData], correct_typ: 'navigator.id.finishEnrollment'),
|
|
16
|
+
response: RegistrationResponse.new(encoded: params[:registrationData]),
|
|
17
|
+
})
|
|
18
|
+
clear_pending_challenge
|
|
19
|
+
|
|
20
|
+
if verifier.save
|
|
21
|
+
user_two_factor_auth_authenticated! 0 # don't need to auth again after registration
|
|
22
|
+
redirect_to after_two_factor_auth_registrations_path_for(current_user)
|
|
23
|
+
else
|
|
24
|
+
flash[:alert] = "Unable to register"
|
|
25
|
+
render :new, status: 406
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
# Does not inherit from ApplicationController because many of those enforce
|
|
3
|
+
# logins on all but a whitelist of pages and the U2F spec requires this
|
|
4
|
+
# respond to a request with no cookies or other auth.
|
|
5
|
+
class TrustedFacetsController < ActionController::Base
|
|
6
|
+
def index
|
|
7
|
+
render text: TwoFactorAuth.facets.to_json, content_type: "application/fido.trusted-apps+json"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
class TwoFactorAuthController < ApplicationController
|
|
3
|
+
include TwoFactorAuth::ApplicationHelper
|
|
4
|
+
include Devise::Controllers::Helpers
|
|
5
|
+
|
|
6
|
+
protect_from_forgery with: :exception
|
|
7
|
+
before_action :authenticate_user!
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def after_two_factor_auth_registrations_path_for(resource)
|
|
12
|
+
signed_in_root_path(resource)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def after_two_factor_auth_authentication_path_for(resource)
|
|
16
|
+
after_sign_in_path_for(resource)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
def user_two_factor_auth_registered?
|
|
4
|
+
current_user.registrations.any?
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def user_two_factor_auth_authenticated! counter
|
|
8
|
+
Registration.authenticated(current_user, counter)
|
|
9
|
+
user_session['two_factor_auth_authenticated'] = Time.now
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def user_two_factor_auth_authenticated?
|
|
13
|
+
user_session['two_factor_auth_authenticated'].present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def user_two_factor_auth_authenticated! counter
|
|
17
|
+
Registration.authenticated(current_user, counter)
|
|
18
|
+
user_session['two_factor_auth_authenticated'] = Time.now
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
module AuthenticationsHelper
|
|
3
|
+
def authentication_request
|
|
4
|
+
@authentication_request ||= AuthenticationRequest.new(
|
|
5
|
+
TwoFactorAuth.trusted_facet_list_url,
|
|
6
|
+
Registration.key_handle_for_authentication(current_user),
|
|
7
|
+
user_session['pending_authentication_request_challenge']
|
|
8
|
+
)
|
|
9
|
+
user_session['pending_authentication_request_challenge'] = @authentication_request.challenge
|
|
10
|
+
@authentication_request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear_pending_challenge
|
|
14
|
+
user_session.delete 'pending_authentication_request_challenge'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
module RegistrationsHelper
|
|
3
|
+
def registration_request
|
|
4
|
+
@registration_request ||= RegistrationRequest.new(
|
|
5
|
+
TwoFactorAuth.trusted_facet_list_url,
|
|
6
|
+
[],
|
|
7
|
+
user_session['pending_registration_request_challenge']
|
|
8
|
+
)
|
|
9
|
+
user_session['pending_registration_request_challenge'] = @registration_request.challenge
|
|
10
|
+
@registration_request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clear_pending_challenge
|
|
14
|
+
user_session.delete 'pending_registration_request_challenge'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'adamantium'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
class AuthenticationRequest
|
|
5
|
+
include Adamantium
|
|
6
|
+
|
|
7
|
+
attr_reader :app_id, :key_handle
|
|
8
|
+
|
|
9
|
+
def initialize app_id, key_handle, challenge=nil
|
|
10
|
+
@app_id = app_id
|
|
11
|
+
@key_handle = key_handle
|
|
12
|
+
@challenge = challenge
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def challenge
|
|
16
|
+
@challenge || TwoFactorAuth::random_encoded_challenge
|
|
17
|
+
end
|
|
18
|
+
memoize :challenge
|
|
19
|
+
|
|
20
|
+
# this matches te browser's u2f api
|
|
21
|
+
def serialized
|
|
22
|
+
{
|
|
23
|
+
appId: app_id,
|
|
24
|
+
keyHandle: TwoFactorAuth.websafe_base64_encode(key_handle),
|
|
25
|
+
challenge: challenge,
|
|
26
|
+
version: U2F_VERSION,
|
|
27
|
+
}.to_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
require 'virtus'
|
|
3
|
+
|
|
4
|
+
# See FIDO U2F "Raw Message Formats" documentation, section 4.3 "Registration
|
|
5
|
+
# Response Message: Success"
|
|
6
|
+
module TwoFactorAuth
|
|
7
|
+
class AuthenticationResponse
|
|
8
|
+
include ActiveModel::Validations
|
|
9
|
+
include Virtus.model
|
|
10
|
+
|
|
11
|
+
attribute :encoded, String
|
|
12
|
+
attribute :raw, String
|
|
13
|
+
|
|
14
|
+
attribute :bitfield, Fixnum
|
|
15
|
+
attribute :user_presence, Boolean
|
|
16
|
+
attribute :counter, Fixnum
|
|
17
|
+
attribute :signature, String
|
|
18
|
+
|
|
19
|
+
validates :bitfield, :user_presence, :counter, :signature, presence: true
|
|
20
|
+
validate :bitfield_correct, :user_is_present
|
|
21
|
+
|
|
22
|
+
def initialize *args, &blk
|
|
23
|
+
super
|
|
24
|
+
decompose_fields
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def decompose_fields
|
|
28
|
+
self.raw = TwoFactorAuth.websafe_base64_decode encoded
|
|
29
|
+
io = StringIO.new raw
|
|
30
|
+
|
|
31
|
+
@bitfield = io.read(1).ord
|
|
32
|
+
@user_presence = @bitfield.ord & 1 == 1
|
|
33
|
+
@counter = io.read(4).unpack("N").first
|
|
34
|
+
@signature = io.read
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bitfield_correct
|
|
38
|
+
if bitfield.ord != 1
|
|
39
|
+
errors.add :bitfield, "bits 1-7 must be set to zero"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def user_is_present
|
|
44
|
+
if !user_presence
|
|
45
|
+
errors.add :user_presence, "must be set"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
require 'virtus'
|
|
3
|
+
|
|
4
|
+
module TwoFactorAuth
|
|
5
|
+
class AuthenticationVerifier
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
include Virtus.model
|
|
8
|
+
|
|
9
|
+
attribute :registration, Registration
|
|
10
|
+
attribute :request, AuthenticationRequest
|
|
11
|
+
attribute :client_data, ClientData
|
|
12
|
+
attribute :response, AuthenticationResponse
|
|
13
|
+
|
|
14
|
+
validate :client_challenge_matches, :client_origin_matches,
|
|
15
|
+
:counter_advances,
|
|
16
|
+
:verify_signature
|
|
17
|
+
validates_associated :client_data, :response
|
|
18
|
+
|
|
19
|
+
def application_parameter
|
|
20
|
+
OpenSSL::Digest::SHA256.new.digest(request.app_id.encode('ASCII-8BIT'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def challenge_parameter
|
|
24
|
+
OpenSSL::Digest::SHA256.new.digest(client_data.json)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def client_challenge_matches
|
|
28
|
+
if client_data.challenge != request.challenge
|
|
29
|
+
errors.add :client_data, "challenge does not match the challenge they were sent"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def client_origin_matches
|
|
34
|
+
if client_data.origin != request.app_id
|
|
35
|
+
errors.add :client_data, "origin does not match the appId they were sent"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def counter_advances
|
|
40
|
+
if response.counter <= registration.counter
|
|
41
|
+
errors.add :response, "does not advance counter - could mean device was cloned"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def digest
|
|
46
|
+
data = [
|
|
47
|
+
application_parameter,
|
|
48
|
+
response.bitfield.chr,
|
|
49
|
+
[response.counter].pack('N'),
|
|
50
|
+
challenge_parameter,
|
|
51
|
+
].join('')
|
|
52
|
+
OpenSSL::Digest::SHA256.new.digest(data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def verify_signature
|
|
56
|
+
ec = OpenSSL::PKey::EC.new('prime256v1')
|
|
57
|
+
ec.public_key = TwoFactorAuth.decode_pubkey registration.public_key
|
|
58
|
+
return false if ec.public_key.nil?
|
|
59
|
+
if !ec.dsa_verify_asn1(digest, response.signature)
|
|
60
|
+
errors.add :response, "signature is not correct"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def counter
|
|
65
|
+
response.counter
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
require 'virtus'
|
|
3
|
+
|
|
4
|
+
module TwoFactorAuth
|
|
5
|
+
class ClientData
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
include Virtus.model
|
|
8
|
+
|
|
9
|
+
attribute :correct_typ, String
|
|
10
|
+
attribute :encoded, String
|
|
11
|
+
attribute :attrs, Hash
|
|
12
|
+
|
|
13
|
+
attribute :json, String
|
|
14
|
+
attribute :typ, String
|
|
15
|
+
attribute :challenge, String
|
|
16
|
+
attribute :origin, String
|
|
17
|
+
attribute :cid_pubkey, String
|
|
18
|
+
|
|
19
|
+
validates :correct_typ, :typ, :challenge, :origin, presence: true
|
|
20
|
+
validates :correct_typ, inclusion: { in: %w{navigator.id.getAssertion navigator.id.finishEnrollment} }
|
|
21
|
+
validate :client_data_has_correct_keys, :typ_correct
|
|
22
|
+
|
|
23
|
+
def initialize *args, &blk
|
|
24
|
+
super
|
|
25
|
+
decompose_attrs if encoded.present?
|
|
26
|
+
raise ArgumentError, "correct_typ is mandatory" if correct_typ.blank?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def decompose_attrs
|
|
30
|
+
self.json = TwoFactorAuth::websafe_base64_decode(encoded)
|
|
31
|
+
self.attrs = JSON.parse(json)
|
|
32
|
+
|
|
33
|
+
self.typ = attrs['typ']
|
|
34
|
+
self.challenge = attrs['challenge']
|
|
35
|
+
self.origin = attrs['origin']
|
|
36
|
+
self.cid_pubkey = attrs['cid_pubkey']
|
|
37
|
+
rescue ArgumentError => e
|
|
38
|
+
errors.add(:encoded, "Can't decode base64: #{e.message}")
|
|
39
|
+
rescue JSON::ParserError => e
|
|
40
|
+
errors.add(:json, "Can't parse json: #{e.message}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def client_data_has_correct_keys
|
|
44
|
+
if attrs.keys != %w{typ challenge origin cid_pubkey}
|
|
45
|
+
errors.add :attrs, "has wrong keys: #{attrs.keys.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def typ_correct
|
|
50
|
+
if typ != correct_typ
|
|
51
|
+
errors.add :typ, "should be navigator.id.getAssertion but is #{typ}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def persisted? ; false ; end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
class Registration < ActiveRecord::Base
|
|
3
|
+
belongs_to :login, polymorphic: true
|
|
4
|
+
|
|
5
|
+
validates :login, :key_handle, :public_key, :certificate, :counter, :last_authenticated_at, presence: true
|
|
6
|
+
|
|
7
|
+
def self.key_handle_for_authentication login
|
|
8
|
+
login.registrations.first.key_handle
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.authenticated login, counter
|
|
12
|
+
reg = login.registrations.first
|
|
13
|
+
reg.last_authenticated_at = Time.now
|
|
14
|
+
reg.counter = counter
|
|
15
|
+
reg.save!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'adamantium'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
class RegistrationRequest
|
|
5
|
+
include Adamantium
|
|
6
|
+
|
|
7
|
+
attr_reader :app_id, :key_handles
|
|
8
|
+
|
|
9
|
+
def initialize app_id=TwoFactorAuth.trusted_facet_list_url, key_handles=[], challenge=nil
|
|
10
|
+
@app_id = app_id
|
|
11
|
+
@key_handles = key_handles
|
|
12
|
+
@challenge = challenge
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def challenge
|
|
16
|
+
@challenge || TwoFactorAuth::random_encoded_challenge
|
|
17
|
+
end
|
|
18
|
+
memoize :challenge
|
|
19
|
+
|
|
20
|
+
def signs
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# this matches te browser's u2f api
|
|
25
|
+
def serialized
|
|
26
|
+
{
|
|
27
|
+
appId: app_id,
|
|
28
|
+
challenge: challenge,
|
|
29
|
+
version: TwoFactorAuth::U2F_VERSION,
|
|
30
|
+
}.to_json
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
require 'virtus'
|
|
3
|
+
|
|
4
|
+
# See FIDO U2F "Raw Message Formats" documentation, section 4.3 "Registration
|
|
5
|
+
# Response Message: Success"
|
|
6
|
+
module TwoFactorAuth
|
|
7
|
+
class RegistrationResponse
|
|
8
|
+
include ActiveModel::Validations
|
|
9
|
+
include Virtus.model
|
|
10
|
+
|
|
11
|
+
USER_PUBKEY_LENGTH = 65
|
|
12
|
+
|
|
13
|
+
attribute :encoded, String
|
|
14
|
+
attribute :raw, String
|
|
15
|
+
|
|
16
|
+
attribute :reserved_byte, Fixnum
|
|
17
|
+
attribute :public_key, String
|
|
18
|
+
attribute :key_handle, String
|
|
19
|
+
attribute :certificate, String
|
|
20
|
+
attribute :signature, String
|
|
21
|
+
|
|
22
|
+
validates :reserved_byte, :public_key, :key_handle, :certificate,
|
|
23
|
+
:signature, presence: true
|
|
24
|
+
validate :reserved_byte_correct, :certificate_valid,
|
|
25
|
+
:certificate_trusted, :public_key_valid
|
|
26
|
+
|
|
27
|
+
def initialize *args, &blk
|
|
28
|
+
super
|
|
29
|
+
decompose_fields if encoded.present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def decompose_fields
|
|
33
|
+
self.raw = TwoFactorAuth::websafe_base64_decode(encoded)
|
|
34
|
+
io = StringIO.new raw
|
|
35
|
+
|
|
36
|
+
self.reserved_byte = io.read(1)
|
|
37
|
+
self.public_key = io.read(USER_PUBKEY_LENGTH)
|
|
38
|
+
key_handle_length = io.readbyte
|
|
39
|
+
self.key_handle = io.read(key_handle_length)
|
|
40
|
+
|
|
41
|
+
at_peek = io.read(4)
|
|
42
|
+
io.seek(-4, IO::SEEK_CUR)
|
|
43
|
+
attestation_certificate_length = 4 + at_peek[2..3].unpack('n').first
|
|
44
|
+
self.certificate = io.read(attestation_certificate_length)
|
|
45
|
+
self.signature = io.read
|
|
46
|
+
rescue ArgumentError => e
|
|
47
|
+
errors.add(:encoded, "Can't decode base64: #{e.message}")
|
|
48
|
+
rescue EOFError => e
|
|
49
|
+
errors.add(:raw, "Can't extract all fields")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def certificate_public_key
|
|
53
|
+
OpenSSL::X509::Certificate.new(certificate).public_key.public_key
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reserved_byte_correct
|
|
57
|
+
if reserved_byte.ord != 5
|
|
58
|
+
errors.add :reserved_byte, "must be 0x05"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def certificate_valid
|
|
63
|
+
OpenSSL::X509::Certificate.new(certificate)
|
|
64
|
+
rescue TypeError # from certificate_public_key not extracting cert and using nil
|
|
65
|
+
errors.add(:certificate, "was not extracted")
|
|
66
|
+
rescue OpenSSL::X509::CertificateError
|
|
67
|
+
errors.add :certificate, "not a valid x509 certificate"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def public_key_valid
|
|
71
|
+
if !TwoFactorAuth.pubkey_valid?(public_key)
|
|
72
|
+
errors.add :public_key, "not a valid public key"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# FIDO raw message formats v1.0 page 5 says "The relying party should also
|
|
77
|
+
# verify that the attestation certificate was issued by a trusted
|
|
78
|
+
# certification authority. The exact process of setting up trusted
|
|
79
|
+
# certification authorities is to be defined by the FIDO Alliance and is
|
|
80
|
+
# outside the scope of this document."
|
|
81
|
+
# This hasn't yet been defined and may turn out to only be a way for the
|
|
82
|
+
# FIDO alliance to extract money from client creators. Or it may turn out to
|
|
83
|
+
# be something that servers want to configure. So this is a placeholder
|
|
84
|
+
# method to remind that this may be important in the future.
|
|
85
|
+
def certificate_trusted
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def persisted? ; false ; end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
require 'virtus'
|
|
3
|
+
|
|
4
|
+
module TwoFactorAuth
|
|
5
|
+
class RegistrationVerifier
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
#include TwoFactorAuth::ValidateAssociated
|
|
8
|
+
include Virtus.model
|
|
9
|
+
|
|
10
|
+
attribute :login
|
|
11
|
+
attribute :request, RegistrationRequest
|
|
12
|
+
attribute :client_data, ClientData
|
|
13
|
+
attribute :response, RegistrationResponse
|
|
14
|
+
|
|
15
|
+
validates :request, :client_data, :response, presence: true
|
|
16
|
+
validate :client_challenge_matches,
|
|
17
|
+
:client_origin_matches,
|
|
18
|
+
:verify_signature
|
|
19
|
+
validates_associated :client_data, :response
|
|
20
|
+
|
|
21
|
+
def application_parameter
|
|
22
|
+
OpenSSL::Digest::SHA256.new.digest(request.app_id.encode('ASCII-8BIT'))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def challenge_parameter
|
|
26
|
+
OpenSSL::Digest::SHA256.new.digest(client_data.json)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def digest
|
|
30
|
+
data = [
|
|
31
|
+
0.chr.encode('ASCII-8BIT'),
|
|
32
|
+
application_parameter,
|
|
33
|
+
challenge_parameter,
|
|
34
|
+
response.key_handle,
|
|
35
|
+
response.public_key,
|
|
36
|
+
].join('')
|
|
37
|
+
OpenSSL::Digest::SHA256.new.digest(data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def client_challenge_matches
|
|
41
|
+
if client_data.challenge != request.challenge
|
|
42
|
+
errors.add :client_data, "challenge does not match the challenge they were sent"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def client_origin_matches
|
|
47
|
+
if client_data.origin != request.app_id
|
|
48
|
+
errors.add :client_data, "origin does not match the appId they were sent"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def verify_signature
|
|
53
|
+
ec = OpenSSL::PKey::EC.new('prime256v1')
|
|
54
|
+
ec.public_key = response.certificate_public_key
|
|
55
|
+
return false if ec.public_key.nil?
|
|
56
|
+
if !ec.dsa_verify_asn1(digest, response.signature)
|
|
57
|
+
errors.add :response, "signature is not correct"
|
|
58
|
+
end
|
|
59
|
+
rescue TypeError # from certificate_public_key not extracting cert and using nil
|
|
60
|
+
errors.add(:response, "certificate was not extracted")
|
|
61
|
+
rescue OpenSSL::PKey::ECError # signature is invalid, not just incorrect
|
|
62
|
+
errors.add(:response, "signature not valid")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def persisted? ; false ; end
|
|
66
|
+
|
|
67
|
+
def save
|
|
68
|
+
if valid?
|
|
69
|
+
persist!
|
|
70
|
+
true
|
|
71
|
+
else
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def registration_attributes
|
|
77
|
+
{
|
|
78
|
+
login: login,
|
|
79
|
+
counter: 0,
|
|
80
|
+
key_handle: response.key_handle,
|
|
81
|
+
public_key: response.public_key,
|
|
82
|
+
certificate: response.certificate,
|
|
83
|
+
last_authenticated_at: Time.now,
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def persist!
|
|
88
|
+
Registration.create!(registration_attributes)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>TwoFactorAuth</title>
|
|
5
|
+
<%= stylesheet_link_tag "two_factor_auth/application", media: "all" %>
|
|
6
|
+
<%= javascript_include_tag "two_factor_auth/application" %>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<p class="notice"><%= notice %></p>
|
|
12
|
+
<p class="alert twofactorauth-status"><%= alert %></p>
|
|
13
|
+
<%= yield %>
|
|
14
|
+
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|