rack-u2f 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 293216cb2f753e7ffae84b3d65aabd4a5b472359
4
+ data.tar.gz: 173f07ee1e0ef64ad696b5994dbd0a993de0bd04
5
+ SHA512:
6
+ metadata.gz: cc091c574805618fa863b6cef886e6ce249151c8b622f08e37bb0fc2ff91ea486f565a08161b4d19b85682560c1b25ee1337fc8931ec9401bc31ca01482e7cb5
7
+ data.tar.gz: f1175bd3ce0366219a8cca9db89b962e063cd47dbfa631a8d13861b26958fa5bb2f08a7fea3279379c4043e5ec876bdf68590beb78a24939f0261fd5e3507208
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ Metrics/LineLength:
2
+ Max: 110
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in rack-u2f.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Eaden McKee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Rack::U2f
2
+
3
+ Note: This gem needs a tidy up and will be properly released by end of Nov 2017
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rack-u2f'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ Rack U2F has two components;
20
+
21
+ A Rack app to register U2F devices
22
+
23
+ and
24
+
25
+ Rack middleware to authenticate against registered U2F devices
26
+
27
+ In rails:
28
+
29
+ In `config/routes.rb`:
30
+
31
+ ```ruby
32
+ mount Rack::U2f::RegistrationServer.new(store: Rack::U2f::RegistrationStore::RedisStore.new), at: '/u2f_registration'
33
+ ```
34
+
35
+ in `config/application.rb`
36
+
37
+ ```ruby
38
+ config.middleware.use Rack::U2f::AuthenticationMiddleware, {
39
+ store: Rack::U2f::RegistrationStore::RedisStore.new,
40
+ exclude_urls: [/\Au2f/, /\A\/\z/]
41
+ }
42
+ ```
43
+
44
+ Currently only a redis store is developed, but other stores such as active record will be easy to add.
45
+
46
+ ## Development
47
+
48
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
49
+
50
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
51
+
52
+ ## Contributing
53
+
54
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eadz/rack-u2f.
55
+
56
+ ## License
57
+
58
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rack/u2f"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/rack/u2f.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'rack/u2f/version'
2
+ require 'rack/u2f/helpers'
3
+ require 'rack/u2f/registration_server'
4
+ require 'rack/u2f/registration_store'
5
+ require 'rack/u2f/authentication_middleware'
6
+
7
+ module Rack
8
+ # :nodoc:
9
+ module U2f
10
+ TEMPLATE_DIR = ::File.join(::File.dirname(__FILE__), 'u2f', 'templates')
11
+ REGISTRATION_TEMPLATE = ::File.read(::File.join(TEMPLATE_DIR, 'registration_page.html.mustache'))
12
+ CHALLENGE_TEMPLATE = ::File.read(::File.join(TEMPLATE_DIR, 'challenge_page.html.mustache'))
13
+ U2FJS = ::File.read(::File.join(TEMPLATE_DIR, 'u2f.js'))
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ module Rack
2
+ module U2f
3
+ # Middleware to authenticate against registered u2f keys
4
+ class AuthenticationMiddleware
5
+ include Helpers
6
+
7
+ def initialize(app, config = nil)
8
+ @app = app
9
+ @store = config[:store] || raise('Please specify a U2F store such as Rack::U2f::RegistrationStore::RedisStore.new')
10
+ @exclude_urls = config[:exclude_urls] || [/\A\/u2f/]
11
+ end
12
+
13
+ def call(env)
14
+ request = Rack::Request.new(env)
15
+ return @app.call(env) if excluded?(request)
16
+ return @app.call(env) if authenticated?(request)
17
+ return resp_auth_from_u2f(request) if request.params['u2f_auth']
18
+ challenge_page(request)
19
+ end
20
+
21
+ private
22
+
23
+ def excluded?(request)
24
+ @exclude_urls.any?{ |exc| request.path =~ exc }
25
+ end
26
+
27
+ def authenticated?(request)
28
+ request.session['u2f_authenticated']
29
+ end
30
+
31
+ def resp_unregistered
32
+ [403, {}, ["Unregistered Device"]]
33
+ end
34
+
35
+ def resp_invalid
36
+ [403, {}, ["Invalid Auth"]]
37
+ end
38
+
39
+ def resp_auth_from_u2f(request)
40
+ u2f_response = U2F::SignResponse.load_from_json(request.params['u2f_auth'])
41
+ registration = @store.get_registration(key_handle: u2f_response.key_handle)
42
+ return unregistered unless registration
43
+ begin
44
+ u2f = U2F::U2F.new(extract_app_id(request))
45
+ u2f.authenticate!(request.session['challenge'], u2f_response,
46
+ Base64.decode64(registration['public_key']),
47
+ registration['counter'])
48
+ rescue U2F::Error => e
49
+ return resp_invalid
50
+ ensure
51
+ request.session.delete('challenge')
52
+ end
53
+
54
+ @store.update_registration(key_handle: u2f_response.key_handle, counter: u2f_response.counter)
55
+ request.session['u2f_authenticated'] = true
56
+ [302, {"Location" => '/'}, []]
57
+ end
58
+
59
+ def challenge_page(request)
60
+ key_handles = @store.key_handles
61
+ return resp_unregistered unless key_handles && key_handles.size > 0
62
+ u2f = U2F::U2F.new(extract_app_id(request))
63
+ sign_requests = u2f.authentication_requests(key_handles)
64
+ challenge = u2f.challenge
65
+ request.session['challenge'] = challenge
66
+
67
+ content = Mustache.render(
68
+ CHALLENGE_TEMPLATE,
69
+ app_id: u2f.app_id.to_json,
70
+ challenge: challenge.to_json,
71
+ sign_requests: sign_requests.to_json,
72
+ u2fjs: U2FJS
73
+ )
74
+
75
+ Rack::Response.new(content)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module Rack
2
+ module U2f
3
+ # Helpers used in the middleware and registration server
4
+ module Helpers
5
+ def extract_app_id(request)
6
+ app_id = request.url.split('/')[0..2].join('/')
7
+ return app_id if [443, 80].include?(request.port)
8
+ app_id + ':' + request.port
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ require 'u2f'
2
+ require 'mustache'
3
+
4
+ module Rack
5
+ module U2f
6
+ # Middleware allow registration of u2f devices
7
+ class RegistrationServer
8
+ include Helpers
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ @store = config[:store]
13
+ raise 'Missing RegistrationMiddleware Config' if @config.nil?
14
+ end
15
+
16
+ def call(env)
17
+ return [403, {}, ['']] unless ENV['ENABLE_U2F_REGISTRATION']
18
+ request = Rack::Request.new(env)
19
+ if request.get?
20
+ generate_registration(request)
21
+ else
22
+ u2f = U2F::U2F.new(extract_app_id(request))
23
+
24
+ response = U2F::RegisterResponse.load_from_json(request.params['response'])
25
+ reg = begin
26
+ u2f.register!(request.session['challenges'], response)
27
+ rescue U2F::Error => e
28
+ return [422, {}, ['Unable to register device']]
29
+ ensure
30
+ request.session.delete('challenges')
31
+ end
32
+ @store.store_registration(
33
+ certificate: reg.certificate,
34
+ key_handle: reg.key_handle,
35
+ public_key: reg.public_key,
36
+ counter: reg.counter
37
+ )
38
+ return [200, {}, ["Registration Successful"]]
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def generate_registration(request)
45
+ u2f = U2F::U2F.new('https://junk.ngrok.io')
46
+ registration_requests = u2f.registration_requests
47
+ request.session['challenges'] = registration_requests.map(&:challenge)
48
+ key_handles = @store.key_handles
49
+ sign_requests = u2f.authentication_requests(key_handles)
50
+
51
+ registration_page(u2f.app_id, registration_requests, sign_requests)
52
+ end
53
+
54
+ def registration_page(app_id, registration_requests, sign_requests)
55
+ content = Mustache.render(
56
+ REGISTRATION_TEMPLATE,
57
+ app_id: app_id.to_json,
58
+ registration_requests: registration_requests.to_json,
59
+ sign_requests: sign_requests.to_json,
60
+ u2fjs: U2FJS
61
+ )
62
+ Rack::Response.new(content)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack/u2f/registration_store/redis_store'
2
+
3
+ module Rack
4
+ module U2f
5
+ # Store to keep track of tokens
6
+ module RegistrationStore
7
+ class AbstractStore
8
+ def initialize(*args)
9
+ end
10
+
11
+ def store_registration(certificate:, key_handle:, public_key:, counter:)
12
+ end
13
+
14
+ def update_registration(key_handle:, counter:)
15
+ end
16
+
17
+ def key_handles
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Rack
2
+ module U2f
3
+ # Store to keep track of u2f data in active record
4
+ module RegistrationStore
5
+ class ActiveRecordStore
6
+ def initialize(ar_model)
7
+ @model = ar_model
8
+ end
9
+
10
+ def store_registration(certificate:, key_handle:, public_key:, counter:)
11
+ end
12
+
13
+ def get_registration(key_handle:)
14
+ end
15
+
16
+ def update_registration(key_handle:, counter:)
17
+ end
18
+
19
+ def key_handles
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ require 'redis'
2
+ module Rack
3
+ module U2f
4
+ # Store to keep track of u2f data in redis
5
+ module RegistrationStore
6
+ class RedisStore
7
+ def initialize(redis_connection = nil)
8
+ @redis = redis_connection || Redis.new
9
+ @hash_key_prefix = 'rack-u2f'
10
+ end
11
+
12
+ def store_registration(certificate:, key_handle:, public_key:, counter:)
13
+ @redis.hset(
14
+ @hash_key_prefix,
15
+ key_handle,
16
+ JSON.dump(
17
+ certificate: certificate,
18
+ public_key: public_key,
19
+ counter: counter
20
+ )
21
+ )
22
+ end
23
+
24
+ def get_registration(key_handle:)
25
+ data = @redis.hget(@hash_key_prefix, key_handle)
26
+ data && JSON.parse(data)
27
+ end
28
+
29
+ def update_registration(key_handle:, counter:)
30
+ existing = get_registration(key_handle: key_handle)
31
+ if existing
32
+ @redis.hset(
33
+ @hash_key_prefix,
34
+ key_handle,
35
+ JSON.dump(
36
+ certificate: existing['certificate'],
37
+ public_key: existing['public_key'],
38
+ counter: counter
39
+ )
40
+ )
41
+ end
42
+ end
43
+
44
+ def key_handles
45
+ @redis.hkeys(@hash_key_prefix) || []
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ <html>
2
+ <head>
3
+ <title>U2F Challenge</title>
4
+ <style type="text/css"><!--
5
+ body {
6
+ background: #232526; /* fallback for old browsers */
7
+ background: -webkit-linear-gradient(to top, #414345, #232526); /* Chrome 10-25, Safari 5.1-6 */
8
+ background: linear-gradient(to top, #414345, #232526); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
9
+ }
10
+ body, html {
11
+ color: #fff;
12
+ font-family: monospace;
13
+ }
14
+ div.main {
15
+ margin-left: auto;
16
+ margin-right: auto;
17
+ width: 60%;
18
+ margin-top: 10%;
19
+ }
20
+ --></style>
21
+ </head>
22
+ <body>
23
+ <form method="post">
24
+ <input type="hidden" name="u2f_auth">
25
+ </form>
26
+
27
+ <script type="text/javascript">
28
+
29
+ {{{u2fjs}}}
30
+
31
+ var appId = {{{app_id}}};
32
+ var challenge = {{{challenge}}};
33
+ var signRequests = {{{sign_requests.as_json}}};
34
+ u2f.sign(appId, challenge, signRequests, function(signResponse) {
35
+ var form, reg, response;
36
+
37
+ if (signResponse.errorCode) {
38
+ return alert("Authentication error: " + signResponse.errorCode);
39
+ }
40
+
41
+ form = document.forms[0];
42
+ response = document.querySelector('[name=u2f_auth]');
43
+
44
+ response.value = JSON.stringify(signResponse);
45
+
46
+ form.submit();
47
+
48
+ });
49
+ </script>
50
+
51
+ <div class="main">
52
+ <h1>U2F Auth</h1>
53
+ <p>Insert your token and authenticate.</p>
54
+ </div>
55
+ </body>
56
+ </html>