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,67 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>The page you were looking for doesn't exist (404)</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
background-color: #EFEFEF;
|
|
9
|
+
color: #2E2F30;
|
|
10
|
+
text-align: center;
|
|
11
|
+
font-family: arial, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
div.dialog {
|
|
16
|
+
width: 95%;
|
|
17
|
+
max-width: 33em;
|
|
18
|
+
margin: 4em auto 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
div.dialog > div {
|
|
22
|
+
border: 1px solid #CCC;
|
|
23
|
+
border-right-color: #999;
|
|
24
|
+
border-left-color: #999;
|
|
25
|
+
border-bottom-color: #BBB;
|
|
26
|
+
border-top: #B00100 solid 4px;
|
|
27
|
+
border-top-left-radius: 9px;
|
|
28
|
+
border-top-right-radius: 9px;
|
|
29
|
+
background-color: white;
|
|
30
|
+
padding: 7px 12% 0;
|
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 100%;
|
|
36
|
+
color: #730E15;
|
|
37
|
+
line-height: 1.5em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
div.dialog > p {
|
|
41
|
+
margin: 0 0 1em;
|
|
42
|
+
padding: 1em;
|
|
43
|
+
background-color: #F7F7F7;
|
|
44
|
+
border: 1px solid #CCC;
|
|
45
|
+
border-right-color: #999;
|
|
46
|
+
border-left-color: #999;
|
|
47
|
+
border-bottom-color: #999;
|
|
48
|
+
border-bottom-left-radius: 4px;
|
|
49
|
+
border-bottom-right-radius: 4px;
|
|
50
|
+
border-top-color: #DADADA;
|
|
51
|
+
color: #666;
|
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
|
|
57
|
+
<body>
|
|
58
|
+
<!-- This file lives in public/404.html -->
|
|
59
|
+
<div class="dialog">
|
|
60
|
+
<div>
|
|
61
|
+
<h1>The page you were looking for doesn't exist.</h1>
|
|
62
|
+
<p>You may have mistyped the address or the page may have moved.</p>
|
|
63
|
+
</div>
|
|
64
|
+
<p>If you are the application owner check the logs for more information.</p>
|
|
65
|
+
</div>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>The change you wanted was rejected (422)</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
background-color: #EFEFEF;
|
|
9
|
+
color: #2E2F30;
|
|
10
|
+
text-align: center;
|
|
11
|
+
font-family: arial, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
div.dialog {
|
|
16
|
+
width: 95%;
|
|
17
|
+
max-width: 33em;
|
|
18
|
+
margin: 4em auto 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
div.dialog > div {
|
|
22
|
+
border: 1px solid #CCC;
|
|
23
|
+
border-right-color: #999;
|
|
24
|
+
border-left-color: #999;
|
|
25
|
+
border-bottom-color: #BBB;
|
|
26
|
+
border-top: #B00100 solid 4px;
|
|
27
|
+
border-top-left-radius: 9px;
|
|
28
|
+
border-top-right-radius: 9px;
|
|
29
|
+
background-color: white;
|
|
30
|
+
padding: 7px 12% 0;
|
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 100%;
|
|
36
|
+
color: #730E15;
|
|
37
|
+
line-height: 1.5em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
div.dialog > p {
|
|
41
|
+
margin: 0 0 1em;
|
|
42
|
+
padding: 1em;
|
|
43
|
+
background-color: #F7F7F7;
|
|
44
|
+
border: 1px solid #CCC;
|
|
45
|
+
border-right-color: #999;
|
|
46
|
+
border-left-color: #999;
|
|
47
|
+
border-bottom-color: #999;
|
|
48
|
+
border-bottom-left-radius: 4px;
|
|
49
|
+
border-bottom-right-radius: 4px;
|
|
50
|
+
border-top-color: #DADADA;
|
|
51
|
+
color: #666;
|
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
|
|
57
|
+
<body>
|
|
58
|
+
<!-- This file lives in public/422.html -->
|
|
59
|
+
<div class="dialog">
|
|
60
|
+
<div>
|
|
61
|
+
<h1>The change you wanted was rejected.</h1>
|
|
62
|
+
<p>Maybe you tried to change something you didn't have access to.</p>
|
|
63
|
+
</div>
|
|
64
|
+
<p>If you are the application owner check the logs for more information.</p>
|
|
65
|
+
</div>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>We're sorry, but something went wrong (500)</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
background-color: #EFEFEF;
|
|
9
|
+
color: #2E2F30;
|
|
10
|
+
text-align: center;
|
|
11
|
+
font-family: arial, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
div.dialog {
|
|
16
|
+
width: 95%;
|
|
17
|
+
max-width: 33em;
|
|
18
|
+
margin: 4em auto 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
div.dialog > div {
|
|
22
|
+
border: 1px solid #CCC;
|
|
23
|
+
border-right-color: #999;
|
|
24
|
+
border-left-color: #999;
|
|
25
|
+
border-bottom-color: #BBB;
|
|
26
|
+
border-top: #B00100 solid 4px;
|
|
27
|
+
border-top-left-radius: 9px;
|
|
28
|
+
border-top-right-radius: 9px;
|
|
29
|
+
background-color: white;
|
|
30
|
+
padding: 7px 12% 0;
|
|
31
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 100%;
|
|
36
|
+
color: #730E15;
|
|
37
|
+
line-height: 1.5em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
div.dialog > p {
|
|
41
|
+
margin: 0 0 1em;
|
|
42
|
+
padding: 1em;
|
|
43
|
+
background-color: #F7F7F7;
|
|
44
|
+
border: 1px solid #CCC;
|
|
45
|
+
border-right-color: #999;
|
|
46
|
+
border-left-color: #999;
|
|
47
|
+
border-bottom-color: #999;
|
|
48
|
+
border-bottom-left-radius: 4px;
|
|
49
|
+
border-bottom-right-radius: 4px;
|
|
50
|
+
border-top-color: #DADADA;
|
|
51
|
+
color: #666;
|
|
52
|
+
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
|
|
57
|
+
<body>
|
|
58
|
+
<!-- This file lives in public/500.html -->
|
|
59
|
+
<div class="dialog">
|
|
60
|
+
<div>
|
|
61
|
+
<h1>We're sorry, but something went wrong.</h1>
|
|
62
|
+
</div>
|
|
63
|
+
<p>If you are the application owner check the logs for more information.</p>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe AuthenticationsHelper do
|
|
5
|
+
def current_user
|
|
6
|
+
@current_user ||= User.create! email: 'user@example.com', password: 'password'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
Registration.create!({
|
|
11
|
+
login: current_user,
|
|
12
|
+
key_handle: "key handle",
|
|
13
|
+
public_key: "public key",
|
|
14
|
+
certificate: "certificate",
|
|
15
|
+
counter: 0,
|
|
16
|
+
last_authenticated_at: Time.now,
|
|
17
|
+
})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
after do
|
|
21
|
+
User.delete_all
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#authentication_request" do
|
|
25
|
+
def user_session
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "creates AuthenticationRequests" do
|
|
30
|
+
authentication_request.must_be_instance_of AuthenticationRequest
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "persists the challenge in the request" do
|
|
34
|
+
ch1 = authentication_request.challenge
|
|
35
|
+
ch2 = authentication_request.challenge
|
|
36
|
+
ch1.must_equal ch2
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "#authentication_request with pending challenge" do
|
|
41
|
+
let(:encoded_challenge) { TwoFactorAuth.websafe_base64_encode('0' * 32) }
|
|
42
|
+
def user_session
|
|
43
|
+
{
|
|
44
|
+
'pending_authentication_request_challenge' => encoded_challenge
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "uses pending challenge" do
|
|
49
|
+
authentication_request.challenge.must_equal encoded_challenge
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe RegistrationsHelper do
|
|
5
|
+
describe "#registration_request" do
|
|
6
|
+
def user_session
|
|
7
|
+
{}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "creates RegistrationRequests" do
|
|
11
|
+
registration_request.must_be_instance_of RegistrationRequest
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "persists the challenge in the request" do
|
|
15
|
+
ch1 = registration_request.challenge
|
|
16
|
+
ch2 = registration_request.challenge
|
|
17
|
+
ch1.must_equal ch2
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "#registration_request with pending challenge" do
|
|
22
|
+
let(:encoded_challenge) { TwoFactorAuth.websafe_base64_encode('0' * 32) }
|
|
23
|
+
def user_session
|
|
24
|
+
{
|
|
25
|
+
'pending_registration_request_challenge' => encoded_challenge
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
it "uses pending challenge" do
|
|
29
|
+
registration_request.challenge.must_equal encoded_challenge
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
describe TwoFactorAuth do
|
|
4
|
+
let(:pubkey) { "\x049\xC6GC\xE6\xD3un;a\xD2\x04\e\x9B,vk\xEB\xDC\xB51\b\x14\x16\a\x92\x8D\xA1\xA92\xD7Z\x17\xB3D\xDF}P\xDA\x9Cg\xEB\xFD7h)N\xC4\xF2\xF1\x10\e\x8A\xC7\x88\x9AM\x17V\xEA\xBEX\x14\xAB" }
|
|
5
|
+
|
|
6
|
+
describe "U2F_VERSION" do
|
|
7
|
+
it "is the only version" do
|
|
8
|
+
TwoFactorAuth::U2F_VERSION.must_equal 'U2F_V2'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "setting facet_domain" do
|
|
13
|
+
it "remembers what is set" do
|
|
14
|
+
TwoFactorAuth.facet_domain = "https://www.example.net"
|
|
15
|
+
TwoFactorAuth.facet_domain.must_equal "https://www.example.net"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "normalizes to remove trailing /" do
|
|
19
|
+
TwoFactorAuth.facet_domain = "https://www.example.net/"
|
|
20
|
+
TwoFactorAuth.facet_domain.must_equal "https://www.example.net"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Yes, after 5 hours of debugging to solve the vague error caused by this,
|
|
24
|
+
# it deserves a redundant test.
|
|
25
|
+
it "doesn't let http domains end in a /" do
|
|
26
|
+
TwoFactorAuth.facet_domain = "http://www.example.net/"
|
|
27
|
+
TwoFactorAuth.facet_domain.must_equal "http://www.example.net"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "raises if you try to use localhost" do
|
|
31
|
+
Proc.new {
|
|
32
|
+
TwoFactorAuth.facet_domain = "http://localhost:3000"
|
|
33
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
34
|
+
Proc.new {
|
|
35
|
+
TwoFactorAuth.facet_domain = "http://localhost:3000/"
|
|
36
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
37
|
+
Proc.new {
|
|
38
|
+
TwoFactorAuth.facet_domain = "http://localhost/"
|
|
39
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "raises if you try to use something.dev" do
|
|
43
|
+
Proc.new {
|
|
44
|
+
TwoFactorAuth.facet_domain = "http://local.dev:3000"
|
|
45
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
46
|
+
Proc.new {
|
|
47
|
+
TwoFactorAuth.facet_domain = "http://local.dev:3000/"
|
|
48
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
49
|
+
Proc.new {
|
|
50
|
+
TwoFactorAuth.facet_domain = "http://local.dev/"
|
|
51
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "raises if you use the default from the initializer" do
|
|
55
|
+
Proc.new {
|
|
56
|
+
TwoFactorAuth.facet_domain = "https://www.example.com"
|
|
57
|
+
}.must_raise(TwoFactorAuth::InvalidFacetDomain)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "setting trusted_facet_list_url" do
|
|
62
|
+
it "can be set explicitly" do
|
|
63
|
+
TwoFactorAuth.trusted_facet_list_url = "https://www.example.net/facets"
|
|
64
|
+
TwoFactorAuth.trusted_facet_list_url.must_equal "https://www.example.net/facets"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "is based on facet_domain otherwise" do
|
|
68
|
+
# this value is persisted across tests...
|
|
69
|
+
TwoFactorAuth.trusted_facet_list_url = nil
|
|
70
|
+
TwoFactorAuth.facet_domain = "https://www.example.net"
|
|
71
|
+
TwoFactorAuth.trusted_facet_list_url.must_equal "https://www.example.net/two_factor_auth/trusted_facets"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "setting facets" do
|
|
76
|
+
it "can be set explicitly" do
|
|
77
|
+
TwoFactorAuth.facets = ["https://www.example.net", "https://staging.example.net"]
|
|
78
|
+
TwoFactorAuth.facets.must_equal ["https://www.example.net", "https://staging.example.net"]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "is just the facet_domain by default" do
|
|
82
|
+
# this value is persisted across tests...
|
|
83
|
+
TwoFactorAuth.facets = nil
|
|
84
|
+
TwoFactorAuth.facet_domain = "https://www.example.net"
|
|
85
|
+
TwoFactorAuth.facets.must_equal ["https://www.example.net"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "is just the facet_domain if empty list" do
|
|
89
|
+
# this value is persisted across tests...
|
|
90
|
+
TwoFactorAuth.facets = []
|
|
91
|
+
TwoFactorAuth.facet_domain = "https://www.example.net"
|
|
92
|
+
TwoFactorAuth.facets.must_equal ["https://www.example.net"]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe ".websafe_base64u_encode" do
|
|
97
|
+
it "encodes data" do
|
|
98
|
+
TwoFactorAuth.websafe_base64_encode("foo").must_equal "Zm9v"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "does not include trailing =s" do
|
|
102
|
+
Base64.urlsafe_encode64("ab").must_equal "YWI="
|
|
103
|
+
TwoFactorAuth.websafe_base64_encode("ab").must_equal "YWI"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe ".websafe_base64u_decode" do
|
|
108
|
+
it "decodes data" do
|
|
109
|
+
TwoFactorAuth.websafe_base64_decode("Zm9v").must_equal "foo"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "does not mind missing trailing =s" do
|
|
113
|
+
Base64.urlsafe_decode64("YWI=").must_equal "ab"
|
|
114
|
+
TwoFactorAuth.websafe_base64_decode("YWI").must_equal "ab"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "#random_encoded_challenge" do
|
|
119
|
+
it "is 32 bytes, encoded" do
|
|
120
|
+
challenge = TwoFactorAuth.random_encoded_challenge
|
|
121
|
+
decoded = TwoFactorAuth.websafe_base64_decode(challenge)
|
|
122
|
+
decoded.length.must_equal 32
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "is different every time" do
|
|
126
|
+
c1 = TwoFactorAuth.random_encoded_challenge
|
|
127
|
+
c2 = TwoFactorAuth.random_encoded_challenge
|
|
128
|
+
c1.wont_equal c2
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "raises if out of entropy" do
|
|
132
|
+
OpenSSL::Random.stub(:pseudo_bytes, Proc.new { raise OpenSSL::Random::RandomError }) do
|
|
133
|
+
Proc.new {
|
|
134
|
+
TwoFactorAuth.random_encoded_challenge
|
|
135
|
+
}.must_raise TwoFactorAuth::CantGenerateRandomNumbers
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "#decode_pubkey" do
|
|
141
|
+
it "returns the Point" do
|
|
142
|
+
point = TwoFactorAuth.decode_pubkey pubkey
|
|
143
|
+
point.to_bn.to_i.must_equal 56657129115817956563260749049282610971311272788676140555509072492610442759820703941373942638593733546160471073247664666784884688116961061627926493680637099
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "raises if invalid" do
|
|
147
|
+
Proc.new {
|
|
148
|
+
TwoFactorAuth.decode_pubkey "invalid key"
|
|
149
|
+
}.must_raise TwoFactorAuth::InvalidPublicKey
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe "#pubkey_valid?" do
|
|
154
|
+
it "is if key is valid" do
|
|
155
|
+
b = TwoFactorAuth.pubkey_valid? pubkey
|
|
156
|
+
b.must_equal true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "is not if key is not a valid key" do
|
|
160
|
+
b = TwoFactorAuth.pubkey_valid? "invalid key"
|
|
161
|
+
b.must_equal false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# I'd like a second test for a well-structured key that is not on the
|
|
165
|
+
# curve, but I can't see how to construct one; OpenSSL::PKey::EC::Point
|
|
166
|
+
# raises "OpenSSL::PKey::EC::Point::Error: point is not on curve" if I try
|
|
167
|
+
# to construct one.
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe AuthenticationRequest do
|
|
5
|
+
let(:app_id) { 'http://twofactorauth.example.com' }
|
|
6
|
+
let(:key_handle) { TwoFactorAuth.websafe_base64_decode "W9G8D38l_29TSKdejU57l4YZsTVeRUUHSuBenSHZ-J_hwL7YI1R_MN20OQETXmWy_tdVDBAnxAct8ys_R-h7cw" }
|
|
7
|
+
|
|
8
|
+
it "holds app id" do
|
|
9
|
+
ar = AuthenticationRequest.new('http://id.example.com', key_handle)
|
|
10
|
+
ar.app_id.must_equal 'http://id.example.com'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'holds key handle' do
|
|
14
|
+
ar = AuthenticationRequest.new('http://id.example.com', 'key handle')
|
|
15
|
+
ar.key_handle.must_equal 'key handle'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "creates random challenges" do
|
|
19
|
+
ar1 = AuthenticationRequest.new(app_id, key_handle)
|
|
20
|
+
ar2 = AuthenticationRequest.new(app_id, key_handle)
|
|
21
|
+
ar1.challenge.wont_equal ar2.challenge
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "uses the challenge given" do
|
|
25
|
+
encoded_challenge = TwoFactorAuth.websafe_base64_encode('0' * 32)
|
|
26
|
+
ar = AuthenticationRequest.new(app_id, key_handle, encoded_challenge)
|
|
27
|
+
ar.challenge.must_equal encoded_challenge
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'encodes the key handle when serialized' do
|
|
31
|
+
ar = AuthenticationRequest.new('http://id.example.com', 'key handle')
|
|
32
|
+
ar.serialized.must_include '"a2V5IGhhbmRsZQ"'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe AuthenticationResponse do
|
|
5
|
+
#parallelize_me!
|
|
6
|
+
|
|
7
|
+
let(:app_id) { "http://local.twofactorauth.io:3000" }
|
|
8
|
+
let(:signatureData) { "AQAAAAUwRQIgaeZw29qOaQ50Wb4a7LjxV32_JR-Bru_0bPm0lIdrK1kCIQCl5_-Lssd4pzZ3tyaLIWIZEKhwZzwhceV2mHN2qzH3mw" }
|
|
9
|
+
let(:response) { AuthenticationResponse.new(encoded: signatureData) }
|
|
10
|
+
|
|
11
|
+
describe "decomposing fields" do
|
|
12
|
+
it "extracts bitfield byte" do
|
|
13
|
+
response.bitfield.must_equal 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "extracts counter" do
|
|
17
|
+
response.counter.must_equal 5
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "extracts signature" do
|
|
21
|
+
response.signature.must_equal "0E\x02 i\xE6p\xDB\xDA\x8Ei\x0EtY\xBE\x1A\xEC\xB8\xF1W}\xBF%\x1F\x81\xAE\xEF\xF4l\xF9\xB4\x94\x87k+Y\x02!\x00\xA5\xE7\xFF\x8B\xB2\xC7x\xA76w\xB7&\x8B!b\x19\x10\xA8pg<!q\xE5v\x98sv\xAB1\xF7\x9B".force_encoding('ASCII-8BIT')
|
|
22
|
+
response.signature.length.must_equal 71
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "starts with signatureData a known size" do
|
|
26
|
+
raw = TwoFactorAuth.websafe_base64_decode signatureData
|
|
27
|
+
raw.length.must_equal 76
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "validation" do
|
|
32
|
+
it "is when correct" do
|
|
33
|
+
response.valid?.must_equal true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "is not when bitfield is wrong" do
|
|
37
|
+
response.bitfield = 8
|
|
38
|
+
response.valid?.must_equal false
|
|
39
|
+
response.errors[:bitfield].wont_be_empty
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module TwoFactorAuth
|
|
4
|
+
describe AuthenticationVerifier do
|
|
5
|
+
#parallelize_me!
|
|
6
|
+
|
|
7
|
+
let(:app_id) { "http://local.fidologin.com:3000" }
|
|
8
|
+
let(:key_handle) { TwoFactorAuth.websafe_base64_decode "fNKqlc0cHr7CcAScmiwJF3qL5WP5YY9vSZR5i474rPWmg8qjTHIckZA_v2Xioj6RB6BNJqzxUVUwG6wfksKXtA" }
|
|
9
|
+
let(:challenge) { "i6M5PrWJbrwwn_25MqHJzbWVdILVCBfg1nLIiVJ_zOs" }
|
|
10
|
+
let(:request) { AuthenticationRequest.new(app_id, key_handle, challenge) }
|
|
11
|
+
let(:registration) { Registration.new({
|
|
12
|
+
key_handle: key_handle,
|
|
13
|
+
public_key: "\x049\xC6GC\xE6\xD3un;a\xD2\x04\e\x9B,vk\xEB\xDC\xB51\b\x14\x16\a\x92\x8D\xA1\xA92\xD7Z\x17\xB3D\xDF}P\xDA\x9Cg\xEB\xFD7h)N\xC4\xF2\xF1\x10\e\x8A\xC7\x88\x9AM\x17V\xEA\xBEX\x14\xAB".force_encoding('ASCII-8BIT'),
|
|
14
|
+
certificate: "0\x82\x02\x1C0\x82\x01\x06\xA0\x03\x02\x01\x02\x02\x04$\xDB\xAB@0\v\x06\t*\x86H\x86\xF7\r\x01\x01\v0.1,0*\x06\x03U\x04\x03\x13#Yubico U2F Root CA Serial 4572006310 \x17\r140801000000Z\x18\x0F20500904000000Z0+1)0'\x06\x03U\x04\x03\f Yubico U2F EE Serial 135032778880Y0\x13\x06\a*\x86H\xCE=\x02\x01\x06\b*\x86H\xCE=\x03\x01\a\x03B\x00\x04\x02\xB0\x94\xBE4}GyA\xC4w\x8E\xBE\xC5\xCAM\xED*G\x9F\xAA\x1Eo\xEC9\xAF\xEB\xDE\f p\xCB[\xD4\xBDi\xC9jx\xE3\xBF\x87Q\xFE\xB5y\e\x8D\xFA\xCA\xC2\x94\x01u\x1C\xB1W\xB9|\t\xE49\x1A6\xA3\x120\x100\x0E\x06\n+\x06\x01\x04\x01\x82\xC4\n\x01\x01\x04\x000\v\x06\t*\x86H\x86\xF7\r\x01\x01\v\x03\x82\x01\x01\x00\xA3c\xAE\x0E\x98:\xF3\v\xBA\xF1,\x8B-\xF3ZY\xBF\x1C\xBBJ\e\x0F\xCBh\xC4\x84U\x84\x90\xF6\x874Xe\xB8\xDB\x02i\xC3F\xE5S\x88L,V\a\xAF\x0E\xA2{\x90\xAC\x8C\xF1\xEFC\x1Fr\xAC\x18\x9D\xB2\x1C\x82I\x14\xBF\x17\x88\xA5Q\x1A3\xD0{L\x8E4d|\xE9\xF6\x1E\x15\x16\xA9\xA9\xB3n\x90\n@ a\xF6\x9A\xA4n\x12\xC52\xB9\x93\xF9B>\xFA\xAAL\xF9\xA3\xB6T\xB4\xDD\xDE\xF2\x92JT\x8F\xD5\x99\x95Q\r\xD4\xF7\xF4\xD9\xA4\xD5!\x93\x87<q\xC9\xB8~\x86\x85>\x9E-\xA7^\x8F\fm(0St\xD4\xEF\xDD^\x14\x96\xF8\xC39\x06\x10{\xD6\x8B\xD65\r\xAA\xD2\xC3x\x11\xEC\xA3\xCAC\xBC\x93\vs@\x97\xDE\xF6\x9Dh\x8D\x94U\fL\xFB\x18\xA9\xE2K\x86\xA2\xE5\xD8\x8FI\x98\x99\xA0\x9B\xCE[\x81\fSl\xAF9\r\xC8\xBD\xDE\x96\r\xF30\xCA\xCA\xBC\x05!\xA1\x83#\x95\x7F\xFE\xBC\xA5\x9C\xA9\v \xB1\r\t\xB5#\x1CX\xC2~\xBAg\x83".force_encoding('ASCII-8BIT'),
|
|
15
|
+
counter: 0,
|
|
16
|
+
}) }
|
|
17
|
+
let(:client_data) { ClientData.new encoded: "eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoiaTZNNVByV0picnd3bl8yNU1xSEp6YldWZElMVkNCZmcxbkxJaVZKX3pPcyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbC5maWRvbG9naW4uY29tOjMwMDAiLCJjaWRfcHVia2V5IjoiIn0", correct_typ: 'navigator.id.getAssertion' }
|
|
18
|
+
let(:signatureData) { "AQAAAAcwRQIhAKCimZ21ZEsqVRLkDW2MRlDVFLFFAdCsvCPRTRCzD_brAiBDrE3KuTNanagb1UtW2YM7tUqwIS-D_MK_EWvVsA-8xQ" }
|
|
19
|
+
let(:response) { AuthenticationResponse.new(encoded: signatureData) }
|
|
20
|
+
let(:verifier) { AuthenticationVerifier.new({
|
|
21
|
+
registration: registration,
|
|
22
|
+
request: request,
|
|
23
|
+
client_data: client_data,
|
|
24
|
+
response: response,
|
|
25
|
+
}) }
|
|
26
|
+
|
|
27
|
+
describe '#application_parameter' do
|
|
28
|
+
it "is the sha256 hash of the app id" do
|
|
29
|
+
verifier.application_parameter.must_equal "Nh\xA3j\x7F\xB2\xEE\xD6D\x1F\x18\t\xF1\xAEL\t\x7F\fjo\n\x80\xDE\xAD{\x9E\x13\x04\xAE\xFD.\xD6".force_encoding('ASCII-8BIT')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#challenge_parameter' do
|
|
34
|
+
it "is the sha256 hash of the base64 decoded json string" do
|
|
35
|
+
verifier.challenge_parameter.must_equal "\x92F\xAAS\t\x88Cxe\xD04E\xD7R\xCA)\x05\x1E\xAA\xCA\x18w\xC3\xBFfl\x17V1M\xC0\x95".force_encoding('ASCII-8BIT')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#digest' do
|
|
40
|
+
it "combines fields to spec" do
|
|
41
|
+
verifier.digest.must_equal "\fH\xF7\x81n41uT6kr\x7F$(\x06A\xF0\xE4\x96\xD6\x12\xB1U\x90\xA2\xB7\a\xBE]\xDF\xFA".force_encoding('ASCII-8BIT')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# describe '#signature_verified?' do
|
|
46
|
+
# it "is if the user's public key signed the digest to produce the signature" do
|
|
47
|
+
# verifier.signature_verified?(registration).must_equal true
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# it "is not otherwise" do
|
|
51
|
+
# request = AuthenticationRequest.new "http://different.app.id", key_handle, challenge
|
|
52
|
+
# verifier = AuthenticationVerifier.new(request, clientData, response)
|
|
53
|
+
# verifier.signature_verified?(registration).must_equal false
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
|
|
57
|
+
describe "validations" do
|
|
58
|
+
it "is valid if everything is right" do
|
|
59
|
+
verifier.valid?.must_equal true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "is not if challenge doesn't match" do
|
|
63
|
+
verifier.client_data.challenge = "different challenge"
|
|
64
|
+
verifier.valid?.must_equal false
|
|
65
|
+
verifier.errors.full_messages.join(' ').must_include 'challenge'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "is not if origin doesn't match" do
|
|
69
|
+
verifier.client_data.origin = "http://different.origin"
|
|
70
|
+
verifier.valid?.must_equal false
|
|
71
|
+
verifier.errors.full_messages.join(' ').must_include 'origin'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "is not if counter doesn't advance" do
|
|
75
|
+
verifier.registration.counter = 1
|
|
76
|
+
verifier.response.counter = 1
|
|
77
|
+
verifier.valid?.must_equal false
|
|
78
|
+
verifier.errors.full_messages.join(' ').must_include 'counter'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|