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,30 @@
|
|
|
1
|
+
Touch your two factor authentication device to complete authentication.
|
|
2
|
+
|
|
3
|
+
<script src="chrome-extension://pfboblefjcgdjicmnffhdgionmgcdmne/u2f-api.js"></script>
|
|
4
|
+
<script>
|
|
5
|
+
//console.log("starting", <%= raw authentication_request.serialized %>);
|
|
6
|
+
u2f.sign([<%= raw authentication_request.serialized %>], function (result) {
|
|
7
|
+
//console.log(result);
|
|
8
|
+
// if keyHandle is wrong, it fails with:
|
|
9
|
+
// {errorCode: 1, errorMessage: "device status code: 2"}
|
|
10
|
+
if (result.errorCode) {
|
|
11
|
+
var errors = document.getElementsByClassName('twofactorauth-status')[0];
|
|
12
|
+
if (result.errorCode == 4) {
|
|
13
|
+
errors.innerHTML = "Unable to authenticate. You registered with a different device. Error code: " + result.errorCode;
|
|
14
|
+
} else {
|
|
15
|
+
errors.innerHTML = "Unable to authenticate. Error code: " + result.errorCode;
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
var form = document.getElementById('two-factor-auth-authentication');
|
|
20
|
+
form.elements.keyHandle.value = result.keyHandle;
|
|
21
|
+
form.elements.signatureData.value = result.signatureData;
|
|
22
|
+
form.elements.clientData.value = result.clientData;
|
|
23
|
+
form.submit();
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
<%= form_tag(two_factor_auth_authentication_path, method: :post, id: 'two-factor-auth-authentication') do %>
|
|
27
|
+
<%= hidden_field_tag :keyHandle %>
|
|
28
|
+
<%= hidden_field_tag :signatureData %>
|
|
29
|
+
<%= hidden_field_tag :clientData %>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
Touch your two factor authentication device to complete registration.
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<script src="chrome-extension://pfboblefjcgdjicmnffhdgionmgcdmne/u2f-api.js"></script>
|
|
6
|
+
<script>
|
|
7
|
+
console.log("starting", <%= raw registration_request.serialized %>);
|
|
8
|
+
u2f.register([<%= raw registration_request.serialized %>], [], function (result) {
|
|
9
|
+
//console.log("callback", result);
|
|
10
|
+
if (result.errorCode) {
|
|
11
|
+
document.getElementsByClassName('twofactorauth-status')[0]
|
|
12
|
+
.innerHTML = "Failed. Error code: " + result.errorCode;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
var form = document.getElementById('two-factor-auth-registration');
|
|
16
|
+
form.elements.clientData.value = result.clientData;
|
|
17
|
+
form.elements.registrationData.value = result.registrationData;
|
|
18
|
+
form.elements.challenge.value = result.challenge;
|
|
19
|
+
form.submit();
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
<%= form_tag(two_factor_auth_registrations_path, method: :post, id: 'two-factor-auth-registration') do %>
|
|
23
|
+
<%= hidden_field_tag :clientData %>
|
|
24
|
+
<%= hidden_field_tag :registrationData %>
|
|
25
|
+
<%= hidden_field_tag :challenge %>
|
|
26
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
==============================================================================
|
|
2
|
+
TwoFactorAuth gem installed. To integrate with your site, follow the steps in
|
|
3
|
+
the README on GitHub or by running:
|
|
4
|
+
|
|
5
|
+
rake two_factor_auth:readme
|
|
6
|
+
==============================================================================
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Set facet_domain to the domain name your users see in the URL bar
|
|
2
|
+
if Rails.env.production?
|
|
3
|
+
TwoFactorAuth.facet_domain = "https://www.example.com"
|
|
4
|
+
|
|
5
|
+
# Optional: If you have in-depth knowledge of the U2F spec and wnat to
|
|
6
|
+
# generate your own Trusted Facet List, delete the production facet_domain
|
|
7
|
+
# setting above and customize this: (you can still use facet_domain in dev)
|
|
8
|
+
# TwoFactorAuth.trusted_facet_list_url = 'https://www.example.com/ExampleAppId'
|
|
9
|
+
elsif Rails.env.staging?
|
|
10
|
+
TwoFactorAuth.facet_domain = "https://staging.example.com"
|
|
11
|
+
else
|
|
12
|
+
# The standard prohibits "localhost" or "local.dev", add an alias to /etc/hosts and use that
|
|
13
|
+
TwoFactorAuth.facet_domain = "http://local2fa.example.com:3000"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Optional: if you want your users to be able to authenticate against multiple
|
|
17
|
+
# domains names or apps, they will *all* have to be served via https and
|
|
18
|
+
# listed here. Yes, 'www.example.com' and 'example.com' count as different.
|
|
19
|
+
TwoFactorAuth.facets = [
|
|
20
|
+
# 'https://example.com',
|
|
21
|
+
# 'https://www.example.com',
|
|
22
|
+
# 'https://www.example.net',
|
|
23
|
+
# 'https://blog.example.com',
|
|
24
|
+
# 'https://admin.example.com',
|
|
25
|
+
# 'https://staging.example.com',
|
|
26
|
+
# 'https://account.example.com',
|
|
27
|
+
|
|
28
|
+
# Use this format for iOS apps:
|
|
29
|
+
# 'ios:bundle-id:example-ios-bundle-id',
|
|
30
|
+
|
|
31
|
+
# Use this format for Android apps, inserting the sha1 (see below):
|
|
32
|
+
# 'android:apk-key-hash:example-sha1-hash-of-apk-signing-cert',
|
|
33
|
+
# To get the sha1, edit this command to include your keystore path and run
|
|
34
|
+
# it in Linux, BSD, or OS X to export the signing certificate in DER format,
|
|
35
|
+
# hash, base64 encode and trim trailing '=':
|
|
36
|
+
# keytool -exportcert -alias androiddebugkey -keystore <your-path-to-apk-signing-keystore> &>2 /dev/null | openssl sha1 -binary | openssl base64 | sed 's/=//g'
|
|
37
|
+
]
|
|
38
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateTwoFactorAuthRegistrations < ActiveRecord::Migration
|
|
2
|
+
def change
|
|
3
|
+
create_table :two_factor_auth_registrations do |t|
|
|
4
|
+
t.references :login, polymorphic: true, null: false, index: true
|
|
5
|
+
t.binary :key_handle, null: false, limit: 65 # Defined in FIDO spec
|
|
6
|
+
t.binary :public_key, null: false, limit: 10.kilobytes
|
|
7
|
+
t.binary :certificate, null: false, limit: 1.megabyte, default: ""
|
|
8
|
+
t.integer :counter, null: false, limit: 5, default: 0 # limit in bytes; no easy way to get a 32b *unsigned*
|
|
9
|
+
t.timestamp :last_authenticated_at, null: false
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :two_factor_auth_registrations, :key_handle
|
|
13
|
+
add_index :two_factor_auth_registrations, :last_authenticated_at
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'rails/generators/base'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
|
|
4
|
+
module TwoFactorAuth
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("../../templates", __FILE__)
|
|
8
|
+
|
|
9
|
+
desc "Creates a TwoFactorAuth migration"
|
|
10
|
+
|
|
11
|
+
# Rails implies Migration is mixed into Base (false), and there's no
|
|
12
|
+
# explanation for why I must define this method. Clearly I'm missing
|
|
13
|
+
# something stupid because there are no docs for this in Rails 4.
|
|
14
|
+
include Rails::Generators::Migration
|
|
15
|
+
def self.next_migration_number(path)
|
|
16
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_migration
|
|
20
|
+
migration_template "migration.rb", "db/migrate/create_two_factor_auth_registrations.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_initializer
|
|
24
|
+
copy_file "initializer.rb", "config/initializers/two_factor_auth.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def show_readme
|
|
28
|
+
readme "README" if behavior == :invoke
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
namespace :two_factor_auth do
|
|
2
|
+
desc "View the TwoFactorAuth readme"
|
|
3
|
+
task :readme do
|
|
4
|
+
begin
|
|
5
|
+
pager = ENV['PAGER']
|
|
6
|
+
pager = 'less' if pager.blank?
|
|
7
|
+
readme = File.expand_path("../../../README.md", __FILE__)
|
|
8
|
+
exec "#{pager} #{readme}"
|
|
9
|
+
rescue Errno::ENOENT
|
|
10
|
+
puts "Sorry, couldn't automatically print it... here's the readme:", readme
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
module AuthenticationHook
|
|
3
|
+
include TwoFactorAuth::ApplicationHelper
|
|
4
|
+
|
|
5
|
+
before_action :two_factor_auth_authentication
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def two_factor_auth_authentication
|
|
10
|
+
if user_signed_in? and !user_two_factor_auth_authenticated?
|
|
11
|
+
redirect_to new_two_factor_auth_authentication_path
|
|
12
|
+
return false
|
|
13
|
+
end
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TwoFactorAuth
|
|
2
|
+
module RegistrationHook
|
|
3
|
+
include TwoFactorAuth::ApplicationHelper
|
|
4
|
+
|
|
5
|
+
before_action :two_factor_auth_registration
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def two_factor_auth_registration
|
|
10
|
+
if user_signed_in? and !user_two_factor_auth_registered?
|
|
11
|
+
redirect_to new_two_factor_auth_registrations_path
|
|
12
|
+
return false
|
|
13
|
+
end
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require "two_factor_auth/engine"
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'active_model'
|
|
7
|
+
require 'active_model/validator'
|
|
8
|
+
|
|
9
|
+
module TwoFactorAuth
|
|
10
|
+
U2F_VERSION = 'U2F_V2'
|
|
11
|
+
|
|
12
|
+
class TwoFactorAuthError < RuntimeError ; end
|
|
13
|
+
class CantGenerateRandomNumbers < TwoFactorAuthError ; end
|
|
14
|
+
class InvalidPublicKey < TwoFactorAuthError ; end
|
|
15
|
+
class InvalidFacetDomain < TwoFactorAuthError ; end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def self.facet_domain= facet_domain
|
|
19
|
+
if facet_domain =~ /localhost(:\d+)?\/?$/
|
|
20
|
+
raise InvalidFacetDomain, "Facet domain can't be localhost, edit /etc/hosts to make a custom hostname"
|
|
21
|
+
end
|
|
22
|
+
if facet_domain =~ /\.dev(:\d+)?\/?$/
|
|
23
|
+
raise InvalidFacetDomain, "Facet domain needs a real TLD, not .dev. Edit /etc/hosts to make a custom hostname"
|
|
24
|
+
end
|
|
25
|
+
if facet_domain == "https://www.example.com"
|
|
26
|
+
raise InvalidFacetDomain, "You need to cusomize the facet_domain in config/initializers/two_factor_auth.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@facet_domain = facet_domain.sub(/\/$/, '')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.facet_domain
|
|
33
|
+
@facet_domain
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.trusted_facet_list_url= url
|
|
37
|
+
@trusted_facet_list_url = url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.trusted_facet_list_url
|
|
41
|
+
@trusted_facet_list_url or "#{facet_domain}/two_factor_auth/trusted_facets"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.facets= facets
|
|
45
|
+
@facets = facets
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.facets
|
|
49
|
+
if @facets.nil? or @facets.empty?
|
|
50
|
+
[facet_domain]
|
|
51
|
+
else
|
|
52
|
+
@facets
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.websafe_base64_encode str
|
|
57
|
+
# PHP code removes trailing =s, don't know why
|
|
58
|
+
Base64.urlsafe_encode64(str).sub(/=+$/,'')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.websafe_base64_decode encoded
|
|
62
|
+
# pad back out to decode
|
|
63
|
+
padded = encoded.ljust((encoded.length/4.0).ceil * 4, '=')
|
|
64
|
+
Base64.urlsafe_decode64(padded)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.random_encoded_challenge
|
|
68
|
+
random = OpenSSL::Random.pseudo_bytes(32)
|
|
69
|
+
TwoFactorAuth::websafe_base64_encode(random)
|
|
70
|
+
rescue OpenSSL::Random::RandomError
|
|
71
|
+
raise CantGenerateRandomNumbers, "Not enough entropy to generate secure challenges"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.decode_pubkey raw
|
|
75
|
+
bn = OpenSSL::BN.new(raw, 2)
|
|
76
|
+
group = OpenSSL::PKey::EC::Group.new('prime256v1')
|
|
77
|
+
point = OpenSSL::PKey::EC::Point.new(group, bn)
|
|
78
|
+
rescue OpenSSL::PKey::EC::Point::Error => e
|
|
79
|
+
raise InvalidPublicKey, "Invalid public key: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.pubkey_valid? raw
|
|
83
|
+
pk = decode_pubkey raw
|
|
84
|
+
pk.on_curve?
|
|
85
|
+
rescue InvalidPublicKey
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module ActiveModel
|
|
91
|
+
module Validations
|
|
92
|
+
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
|
|
93
|
+
def validate_each(record, attribute, value)
|
|
94
|
+
if Array.wrap(value).reject {|r| r.valid?}.any?
|
|
95
|
+
record.errors.add(attribute, value.errors.full_messages.join("; "), options.merge(:value => value))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
module ClassMethods
|
|
101
|
+
# Validates whether the associated object or objects are all valid.
|
|
102
|
+
# Works with any kind of association.
|
|
103
|
+
#
|
|
104
|
+
# class Book < ActiveRecord::Base
|
|
105
|
+
# has_many :pages
|
|
106
|
+
# belongs_to :library
|
|
107
|
+
#
|
|
108
|
+
# validates_associated :pages, :library
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# WARNING: This validation must not be used on both ends of an association.
|
|
112
|
+
# Doing so will lead to a circular dependency and cause infinite recursion.
|
|
113
|
+
#
|
|
114
|
+
# NOTE: This validation will not fail if the association hasn't been
|
|
115
|
+
# assigned. If you want to ensure that the association is both present and
|
|
116
|
+
# guaranteed to be valid, you also need to use +validates_presence_of+.
|
|
117
|
+
#
|
|
118
|
+
# Configuration options:
|
|
119
|
+
#
|
|
120
|
+
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
|
|
121
|
+
# * <tt>:on</tt> - Specifies when this validation is active. Runs in all
|
|
122
|
+
# validation contexts by default (+nil+), other options are <tt>:create</tt>
|
|
123
|
+
# and <tt>:update</tt>.
|
|
124
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
|
125
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
|
126
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
|
127
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
|
128
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
|
|
129
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
|
130
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
|
131
|
+
# method, proc or string should return or evaluate to a +true+ or +false+
|
|
132
|
+
# value.
|
|
133
|
+
def validates_associated(*attr_names)
|
|
134
|
+
validates_with AssociatedValidator, _merge_attributes(attr_names)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
module ActionDispatch::Routing
|
|
141
|
+
class Mapper
|
|
142
|
+
def two_factor_auth_for resource
|
|
143
|
+
begin
|
|
144
|
+
klass = resource.to_s.classify.constantize
|
|
145
|
+
rescue NameError
|
|
146
|
+
warn "You included two_factor_auth_for #{resource.inspect} in your routes but there is no model defined in your system"
|
|
147
|
+
end
|
|
148
|
+
namespace :two_factor_auth do
|
|
149
|
+
resources(:registrations, only: [:new, :create])
|
|
150
|
+
resource(:authentication, only: [:new, :create])
|
|
151
|
+
resources(:trusted_facets, only: [:index])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe AuthenticationsController do
|
|
5
|
+
let(:current_user) { User.create!(email: 'user@example.com', password: 'password') }
|
|
6
|
+
let(:registration) { Registration.create!({
|
|
7
|
+
login: current_user,
|
|
8
|
+
key_handle: TwoFactorAuth.websafe_base64_decode("fNKqlc0cHr7CcAScmiwJF3qL5WP5YY9vSZR5i474rPWmg8qjTHIckZA_v2Xioj6RB6BNJqzxUVUwG6wfksKXtA"),
|
|
9
|
+
public_key: TwoFactorAuth.websafe_base64_decode("BDnGR0Pm03VuO2HSBBubLHZr69y1MQgUFgeSjaGpMtdaF7NE331Q2pxn6_03aClOxPLxEBuKx4iaTRdW6r5YFKs"),
|
|
10
|
+
certificate: TwoFactorAuth.websafe_base64_decode("MIICHDCCAQagAwIBAgIEJNurQDALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCsxKTAnBgNVBAMMIFl1YmljbyBVMkYgRUUgU2VyaWFsIDEzNTAzMjc3ODg4MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEArCUvjR9R3lBxHeOvsXKTe0qR5-qHm_sOa_r3gwgcMtb1L1pyWp447-HUf61eRuN-srClAF1HLFXuXwJ5DkaNqMSMBAwDgYKKwYBBAGCxAoBAQQAMAsGCSqGSIb3DQEBCwOCAQEAo2OuDpg68wu68SyLLfNaWb8cu0obD8toxIRVhJD2hzRYZbjbAmnDRuVTiEwsVgevDqJ7kKyM8e9DH3KsGJ2yHIJJFL8XiKVRGjPQe0yONGR86fYeFRapqbNukApAIGH2mqRuEsUyuZP5Qj76qkz5o7ZUtN3e8pJKVI_VmZVRDdT39Nmk1SGThzxxybh-hoU-ni2nXo8MbSgwU3TU791eFJb4wzkGEHvWi9Y1DarSw3gR7KPKQ7yTC3NAl972nWiNlFUMTPsYqeJLhqLl2I9JmJmgm85bgQxTbK85Dci93pYN8zDKyrwFIaGDI5V__rylnKkLILENCbUjHFjCfrpngw"),
|
|
11
|
+
counter: 3,
|
|
12
|
+
last_authenticated_at: Time.now,
|
|
13
|
+
}) }
|
|
14
|
+
let(:challenge) { "430x3zbNg7tdHBds3_aXoSjp81xWe_2eZoEgR856tv8" }
|
|
15
|
+
let(:clientData) { "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiNDMweDN6Yk5nN3RkSEJkczNfYVhvU2pwODF4V2VfMmVab0VnUjg1NnR2OCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbC5maWRvbG9naW4uY29tOjMwMDAiLCJjaWRfcHVia2V5IjoiIn0" }
|
|
16
|
+
let(:signatureData) { "AQAAABAwRgIhAPueB6u8s63myrtQBT7KNOR3c4CVoNPVAiEkSOB8WGzqAiEA5zYbDQopgsVUl3d3pC947pKFSSIJs00ouC3xn3m7Pxo" }
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
TwoFactorAuth.trusted_facet_list_url = "http://local.fidologin.com:3000"
|
|
20
|
+
register_as current_user, registration
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "#new" do
|
|
24
|
+
it "has a form linking to create" do
|
|
25
|
+
get :new
|
|
26
|
+
assert_response :success
|
|
27
|
+
assert_select "form[action='/two_factor_auth/authentication']"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "does not prompt for re-authentication if you already have" do
|
|
31
|
+
authenticate_as current_user, registration
|
|
32
|
+
get :new
|
|
33
|
+
assert_response :redirect
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe "#create" do
|
|
38
|
+
it "clears the pending challenge" do
|
|
39
|
+
controller.stub(:user_session, { 'pending_authentication_request_challenge' => challenge }) do
|
|
40
|
+
post :create, keyHandle: TwoFactorAuth.websafe_base64_encode(registration.key_handle), clientData: clientData, signatureData: signatureData
|
|
41
|
+
controller.user_session.wont_include 'pending_authentication_request_challenge'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "success" do
|
|
46
|
+
|
|
47
|
+
it "creates a Registration when the challenge is verified" do
|
|
48
|
+
controller.stub(:user_session, { 'pending_authentication_request_challenge' => challenge }) do
|
|
49
|
+
post :create, keyHandle: TwoFactorAuth.websafe_base64_encode(registration.key_handle), clientData: clientData, signatureData: signatureData
|
|
50
|
+
assert_response :redirect
|
|
51
|
+
assert_redirected_to '/'
|
|
52
|
+
controller.send(:user_two_factor_auth_authenticated?).must_equal true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "failure" do
|
|
59
|
+
it "renders an error when challenge is not verified" do
|
|
60
|
+
controller.stub(:user_session, { 'pending_authentication_request_challenge' => 'not matched' }) do
|
|
61
|
+
post :create, keyHandle: TwoFactorAuth.websafe_base64_encode(registration.key_handle), clientData: clientData, signatureData: signatureData
|
|
62
|
+
controller.send(:user_two_factor_auth_authenticated?).must_equal false
|
|
63
|
+
response.status.must_equal 406
|
|
64
|
+
response.body.must_include 'Unable to authenticate'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe RegistrationsController do
|
|
5
|
+
let(:current_user) { User.create!(email: 'user@example.com', password: 'password') }
|
|
6
|
+
let(:clientData) { "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6IjVmeGF6cWFuUkh0ZDdBdEVIQkd4eERwU2o2bWRCRjI2WEY0eGRBOW03SnciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWwuZmlkb2xvZ2luLmNvbTozMDAwIiwiY2lkX3B1YmtleSI6IiJ9" }
|
|
7
|
+
let(:registrationData) { "BQQqdFC3zhANYW9DmErAjFQYZjBExK22PLx-ViMOch04-wZ990aqOcF2gxS5gzSUDKzpPGXpliMk3UoXgYlC2QNuQGbQ4E5v_UrLCzT58SXg902p9JXmLboTF42QkuZXIbdea_97h96lVovJ7xrA-iWTrZiOSRVZoBZsTrCW64XMrUcwggIcMIIBBqADAgECAgQk26tAMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKzEpMCcGA1UEAwwgWXViaWNvIFUyRiBFRSBTZXJpYWwgMTM1MDMyNzc4ODgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQCsJS-NH1HeUHEd46-xcpN7SpHn6oeb-w5r-veDCBwy1vUvWnJanjjv4dR_rV5G436ysKUAXUcsVe5fAnkORo2oxIwEDAOBgorBgEEAYLECgEBBAAwCwYJKoZIhvcNAQELA4IBAQCjY64OmDrzC7rxLIst81pZvxy7ShsPy2jEhFWEkPaHNFhluNsCacNG5VOITCxWB68OonuQrIzx70MfcqwYnbIcgkkUvxeIpVEaM9B7TI40ZHzp9h4VFqmps26QCkAgYfaapG4SxTK5k_lCPvqqTPmjtlS03d7ykkpUj9WZlVEN1Pf02aTVIZOHPHHJuH6GhT6eLadejwxtKDBTdNTv3V4UlvjDOQYQe9aL1jUNqtLDeBHso8pDvJMLc0CX3vadaI2UVQxM-xip4kuGouXYj0mYmaCbzluBDFNsrzkNyL3elg3zMMrKvAUhoYMjlX_-vKWcqQsgsQ0JtSMcWMJ-umeDMEUCIQCPkI4L_gHM88JrqJj_ZNRghQyC0gJyCC9RBrnfI2mDTwIgPOuEiD1AOfRaGO_EaHi-z4XyIGDhkG8-BYH-syVY5_o" }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
TwoFactorAuth.trusted_facet_list_url = "http://local.fidologin.com:3000"
|
|
11
|
+
sign_in current_user
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#new" do
|
|
15
|
+
it "has a form linking to create" do
|
|
16
|
+
get :new
|
|
17
|
+
assert_response :success
|
|
18
|
+
assert_select "form[action='/two_factor_auth/registrations']"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#create" do
|
|
23
|
+
it "clears the pending challenge" do
|
|
24
|
+
controller.stub(:user_session, { 'pending_registration_request_challenge' => '5fxazqanRHtd7AtEHBGxxDpSj6mdBF26XF4xdA9m7Jw' }) do
|
|
25
|
+
post :create, clientData: clientData, registrationData: registrationData
|
|
26
|
+
controller.user_session.wont_include 'pending_registration_request_challenge'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "success" do
|
|
31
|
+
|
|
32
|
+
it "creates a Registration when the challenge is verified" do
|
|
33
|
+
controller.stub(:user_session, { 'pending_registration_request_challenge' => '5fxazqanRHtd7AtEHBGxxDpSj6mdBF26XF4xdA9m7Jw' }) do
|
|
34
|
+
Registration.count.must_equal 0
|
|
35
|
+
post :create, clientData: clientData, registrationData: registrationData
|
|
36
|
+
assert_response :redirect
|
|
37
|
+
assert_redirected_to '/'
|
|
38
|
+
current_user.registrations.count.must_equal 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "failure" do
|
|
45
|
+
it "renders an error when challenge is not verified" do
|
|
46
|
+
controller.stub(:user_session, { 'pending_registration_request_challenge' => 'not matched' }) do
|
|
47
|
+
Registration.count.must_equal 0
|
|
48
|
+
post :create, clientData: clientData, registrationData: registrationData
|
|
49
|
+
response.status.must_equal 406
|
|
50
|
+
Registration.count.must_equal 0
|
|
51
|
+
response.body.must_include 'Unable to register'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe TrustedFacetsController do
|
|
5
|
+
it "returns the list of facets as json" do
|
|
6
|
+
TwoFactorAuth.facets = [ 'https://example.com', 'https://admin.example.com' ]
|
|
7
|
+
get :index
|
|
8
|
+
facets = JSON::parse(response.body)
|
|
9
|
+
facets.must_equal TwoFactorAuth.facets
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "has the U2F mimetype" do
|
|
13
|
+
get :index
|
|
14
|
+
response.content_type.must_equal "application/fido.trusted-apps+json"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
== README
|
|
2
|
+
|
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
|
4
|
+
application up and running.
|
|
5
|
+
|
|
6
|
+
Things you may want to cover:
|
|
7
|
+
|
|
8
|
+
* Ruby version
|
|
9
|
+
|
|
10
|
+
* System dependencies
|
|
11
|
+
|
|
12
|
+
* Configuration
|
|
13
|
+
|
|
14
|
+
* Database creation
|
|
15
|
+
|
|
16
|
+
* Database initialization
|
|
17
|
+
|
|
18
|
+
* How to run the test suite
|
|
19
|
+
|
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
|
21
|
+
|
|
22
|
+
* Deployment instructions
|
|
23
|
+
|
|
24
|
+
* ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
|
28
|
+
<tt>rake doc:app</tt>.
|
data/test/dummy/Rakefile
ADDED
|
@@ -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,8 @@
|
|
|
1
|
+
class User < ActiveRecord::Base
|
|
2
|
+
# Include default devise modules. Others available are:
|
|
3
|
+
# :confirmable, :lockable, :timeoutable and :omniauthable
|
|
4
|
+
devise :database_authenticatable, :registerable,
|
|
5
|
+
:recoverable, :rememberable, :trackable, :validatable
|
|
6
|
+
|
|
7
|
+
has_many :registrations, inverse_of: :login, as: :login, dependent: :destroy, class_name: "TwoFactorAuth::Registration"
|
|
8
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Dummy</title>
|
|
5
|
+
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
|
6
|
+
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<div class="alert twofactorauth-status"><%= alert %></div>
|
|
12
|
+
|
|
13
|
+
<%= yield %>
|
|
14
|
+
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<h1>Secrets</h1>
|
|
2
|
+
|
|
3
|
+
<ul>
|
|
4
|
+
<li><a href="#">Startup ideas (NDA required)</a></li>
|
|
5
|
+
<li><a href="#">Stock tips</a></li>
|
|
6
|
+
<li><a href="#">18 and 1/2 minutes of audio tape</a></li>
|
|
7
|
+
<li><a href="#">Great American novel (first draft)</a></li>
|
|
8
|
+
<li><a href="#">Crushes on boys</a></li>
|
|
9
|
+
<li><a href="#">IRS email backup</a></li>
|
|
10
|
+
</ul>
|