devise_fido_usf 0.1.0

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.
@@ -0,0 +1,54 @@
1
+ class Devise::FidoUsfAuthenticationsController < DeviseController
2
+ before_action :find_resource_and_verify_password, only: [:new, :create]
3
+
4
+ def new
5
+ key_handles = @resource.fido_usf_devices.map(&:key_handle)
6
+ @app_id = helpers.u2f.app_id
7
+ @sign_requests = helpers.u2f.authentication_requests(key_handles)
8
+ @challenge = helpers.u2f.challenge
9
+ session[:"#{resource_name}_u2f_challenge"] = @challenge
10
+ render :new
11
+ end
12
+
13
+ def create
14
+ begin
15
+ response = U2F::SignResponse.load_from_json(params[:response])
16
+ rescue TypeError
17
+ return redirect_to root_path
18
+ end
19
+
20
+ registration = @resource.fido_usf_devices.find_by_key_handle(response.key_handle)
21
+ return 'Need to register first' unless registration
22
+
23
+ begin
24
+
25
+ #helpers.u2f.authenticate!(session[:"#{resource_name}_u2f_challenge"], response, Base64.decode64(registration.public_key), registration.counter)
26
+ helpers.u2f.authenticate!(session[:"#{resource_name}_u2f_challenge"], response, registration.public_key, registration.counter)
27
+ registration.update(counter: response.counter, last_authenticated_at: Time.now)
28
+
29
+ # Remember the user (if applicable)
30
+ @resource.remember_me = Devise::TRUE_VALUES.include?(session[:"#{resource_name}_remember_me"]) if @resource.respond_to?(:remember_me=)
31
+ sign_in(resource_name, @resource)
32
+
33
+ set_flash_message(:notice, :signed_in) if is_navigational_format?
34
+ rescue U2F::Error => e
35
+ flash[:error] = "Unable to authenticate: #{e.class.name}"
36
+ return redirect_to root_path
37
+ ensure
38
+ session.delete(:"#{resource_name}_u2f_challenge")
39
+ end
40
+
41
+ respond_with resource, :location => after_sign_in_path_for(@resource)
42
+ end
43
+
44
+ private
45
+ def find_resource_and_verify_password
46
+ @resource = send("current_#{resource_name}")
47
+ if @resource.nil?
48
+ @resource = resource_class.find_by_id(session["#{resource_name}_id"])
49
+ end
50
+ if @resource.nil? || !Devise::TRUE_VALUES.include?(session[:"#{resource_name}_password_checked"])
51
+ redirect_to root_path
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ class Devise::FidoUsfRegistrationsController < ApplicationController
2
+ before_action :authenticate_user!
3
+
4
+ def new
5
+ @registration_requests = helpers.u2f.registration_requests
6
+ session[:challenges] = @registration_requests.map(&:challenge)
7
+ key_handles = current_user.fido_usf_devices.map(&:key_handle)
8
+ @sign_requests = helpers.u2f.authentication_requests(key_handles)
9
+ @app_id = helpers.u2f.app_id
10
+ render :new
11
+ end
12
+
13
+ # Show a list of all registered devices
14
+ def show
15
+ @devices = current_user.fido_usf_devices.all
16
+ render :show
17
+ end
18
+
19
+ def destroy
20
+ device = current_user.fido_usf_devices.find(params[:id])
21
+ @fade_out_id = device.id
22
+ device.destroy
23
+ @devices = current_user.fido_usf_devices.all
24
+ flash[:success] = I18n.t('fido_usf.flashs.device.removed')
25
+ respond_to do |format|
26
+ format.js
27
+ format.html { redirect_to user_fido_usf_registration_url }
28
+ end
29
+ end
30
+
31
+ def create
32
+ begin
33
+ response = U2F::RegisterResponse.load_from_json(params[:response])
34
+ reg = helpers.u2f.register!(session[:challenges], response)
35
+
36
+ pubkey = reg.public_key
37
+ pubkey = Base64.decode64(reg.public_key) unless pubkey.bytesize == 65 && pubkey.byteslice(0) != "\x04"
38
+
39
+ FidoUsf::FidoUsfDevice.create!(
40
+ user: current_user,
41
+ name: 'Unnamed 1',
42
+ certificate: reg.certificate,
43
+ key_handle: reg.key_handle,
44
+ public_key: pubkey,
45
+ counter: reg.counter,
46
+ last_authenticated_at: Time.now)
47
+ flash[:success] = I18n.t('fido_usf.flashs.device.registered')
48
+ rescue U2F::Error => e
49
+ @error_message = "Unable to register: #{e.class.name}"
50
+ flash[:error] = @error_message
51
+ ensure
52
+ session.delete(:challenges)
53
+ end
54
+
55
+ redirect_to user_fido_usf_registration_path()
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ module FidoUsf
2
+ class FidoUsfDevice < ActiveRecord::Base
3
+ belongs_to :user, polymorphic: true
4
+
5
+ validates :user, :name, :key_handle, :public_key, :certificate, :counter, :last_authenticated_at, presence: true
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ <h2>Authenticate key</h2>
2
+ <p>Please insert one of your registered keys and press the button within 15 seconds</p>
3
+ <p id="waiting">Waiting...</p>
4
+ <p id="error" style="display: none;"></p>
5
+ <%= form_tag user_fido_usf_authentication_path(), method: 'post' do %>
6
+ <%= hidden_field_tag :response %>
7
+ <% end %>
8
+ <script>
9
+ var appId = <%= @app_id.to_json.html_safe %>;
10
+ var signRequests = <%= @sign_requests.to_json.html_safe %>;
11
+ var challenge = <%= @challenge.to_json.html_safe %>;
12
+ var $waiting = document.getElementById('waiting');
13
+ var $error = document.getElementById('error');
14
+ var errorMap = {
15
+ 1: 'Unknown error, try again',
16
+ 2: "Bad request error, try again" ,
17
+ 3: "This key isn't supported, please try another one",
18
+ 4: 'The device is is not registered, please register first.',
19
+ 5: 'Authentication timed out. Please reload to try again.'
20
+ };
21
+ var setError = function(code) {
22
+ $waiting.style.display = 'none';
23
+ $error.style.display = 'block';
24
+ $error.innerHTML = errorMap[code];
25
+ };
26
+
27
+ u2f.sign(appId, challenge, signRequests, function(signResponse) {
28
+ var form, reg;
29
+
30
+ if (signResponse.errorCode) {
31
+ return setError(signResponse.errorCode);
32
+ }
33
+
34
+ form = document.forms[0];
35
+ response = document.querySelector('[name=response]');
36
+
37
+ response.value = JSON.stringify(signResponse);
38
+
39
+ form.submit();
40
+ }, 15);
41
+ </script>
@@ -0,0 +1,5 @@
1
+ <tr id="device_<%= device.id %>">
2
+ <td><%= device.name %></td>
3
+ <td><%= l(device.last_authenticated_at, format: :long) %></td>
4
+ <td><%= link_to 'Delete', user_fido_usf_registration_path(id: device.id), remote: true, :method => :delete, data: {confirm: "Should device #{device.name} be deleted?" } %></td>
5
+ </tr>
@@ -0,0 +1,14 @@
1
+ <div class="new-device"></div>
2
+
3
+ <table id class="table table-bordered table-striped">
4
+ <thead>
5
+ <tr>
6
+ <th class="col-md-4"><%= FidoUsf::FidoUsfDevice.human_attribute_name('name') %></th>
7
+ <th class="col-md-3"><%= FidoUsf::FidoUsfDevice.human_attribute_name('last_authenticated_at') %></th>
8
+ <th class="col-md-2"><%= t('common.actions') %></th>
9
+ </tr>
10
+ </thead>
11
+ <tbody id="devices">
12
+ <%= render partial: 'devise/fido_usf_registrations/device', collection: @devices %>
13
+ </tbody>
14
+ </table>
@@ -0,0 +1,3 @@
1
+ $('#device_<%= @fade_out_id %>').fadeOut(400, function () {
2
+ $('#devices').html('<%= j(render(partial: 'devise/fido_usf_registrations/device', collection: @devices)) %>')
3
+ } );
@@ -0,0 +1,43 @@
1
+ <h2>Register key</h2>
2
+ <p>Please insert the key and press the button within 15 seconds</p>
3
+ <p id="waiting">Waiting...</p>
4
+ <p id="error" style="display: none;"></p>
5
+
6
+ <%= form_tag user_fido_usf_registration_path(), method: 'post' do %>
7
+ <%= hidden_field_tag :response %>
8
+ <% end %>
9
+
10
+ <script>
11
+ var appId = <%= @app_id.to_json.html_safe %>;
12
+ var registerRequests = <%= @registration_requests.to_json.html_safe %>;
13
+ var signRequests = <%= @sign_requests.to_json.html_safe %>;
14
+ var $waiting = document.getElementById('waiting');
15
+ var $error = document.getElementById('error');
16
+ var errorMap = {
17
+ 1: 'Unknown error, try again',
18
+ 2: "Bad request error, try again" ,
19
+ 3: "This key isn't supported, please try another one",
20
+ 4: 'The device is already registered, please login',
21
+ 5: 'Authentication timed out. Please reload to try again.'
22
+ };
23
+ var setError = function(code) {
24
+ $waiting.style.display = 'none';
25
+ $error.style.display = 'block';
26
+ $error.innerHTML = errorMap[code];
27
+ };
28
+
29
+ u2f.register(appId, registerRequests, signRequests, function(registerResponse) {
30
+ var form, reg;
31
+
32
+ if (registerResponse.errorCode) {
33
+ return setError(registerResponse.errorCode);
34
+ }
35
+
36
+ form = document.forms[0];
37
+ response = document.querySelector('[name=response]');
38
+
39
+ response.value = JSON.stringify(registerResponse);
40
+
41
+ form.submit();
42
+ }, 15);
43
+ </script>
@@ -0,0 +1,5 @@
1
+ <h2> FIDO U2F Devices</h2>
2
+ <p>List of registered devices:</p>
3
+ <%= render 'devise/fido_usf_registrations/devices' %>
4
+ <p><%= link_to 'Back', root_path() %></p>
5
+ <p><%= link_to 'Add', new_user_fido_usf_registration_path() %></p>
@@ -0,0 +1,24 @@
1
+ en:
2
+ fido_usf:
3
+ pages:
4
+ devices: "2FA Devices"
5
+
6
+ flashs:
7
+ device:
8
+ removed: "Device removed."
9
+ registered: "New 2FA device successfully registered."
10
+
11
+ common:
12
+ actions: 'Actions'
13
+
14
+ forms:
15
+ device:
16
+ add: "Add 2FA Device"
17
+ delete: "Delete"
18
+ delete_confirm: "Do you really want to delete the 2FA device \"%{device_name}\"?"
19
+
20
+ activerecord:
21
+ attributes:
22
+ fido_usf/fido_usf_device:
23
+ last_authenticated_at: "Last used"
24
+
@@ -0,0 +1,13 @@
1
+ module DeviseFidoUsf
2
+ module Controllers
3
+ autoload :Helpers, 'devise_fido_usf/controllers/helpers'
4
+ end
5
+ end
6
+
7
+ require 'devise'
8
+ require 'u2f'
9
+ require 'devise_fido_usf/routes'
10
+ require 'devise_fido_usf/rails'
11
+
12
+ Devise.add_module :fido_usf_registerable, :model => 'devise_fido_usf/models/fido_usf_registerable', :controller => :fido_usf_registrations, :route => :fido_usf_registration
13
+ Devise.add_module :fido_usf_authenticatable, :model => 'devise_fido_usf/models/fido_usf_authenticatable', :controller => :fido_usf_authentications, :route => :fido_usf_authentication
@@ -0,0 +1,54 @@
1
+ # Code loosely based on authy-devise gem from https://github.com/authy/authy-devise
2
+
3
+ module DeviseFidoUsf
4
+ module Controllers
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :check_request_and_redirect_to_verify_fido_usf, :if => :is_user_signing_in?
10
+ end
11
+
12
+ private
13
+ def is_devise_sessions_controller?
14
+ self.class == Devise::SessionsController || self.class.ancestors.include?(Devise::SessionsController)
15
+ end
16
+
17
+ def is_user_signing_in?
18
+ if devise_controller? && signed_in?(resource_name) &&
19
+ is_devise_sessions_controller? &&
20
+ self.action_name == "create"
21
+ return true
22
+ end
23
+
24
+ return false
25
+ end
26
+
27
+ def check_request_and_redirect_to_verify_fido_usf
28
+ if signed_in?(resource_name) && warden.session(resource_name)[:with_fido_usf_authentication]
29
+ # login with 2fa
30
+ id = warden.session(resource_name)[:id]
31
+
32
+ return_to = session["#{resource_name}_return_to"]
33
+ remember_me = Devise::TRUE_VALUES.include?(sign_in_params[:remember_me])
34
+ sign_out
35
+
36
+ # It is secure to put these information in a Rails 5 session
37
+ # because cookies are signed and encrypted.
38
+ session["#{resource_name}_id"] = id
39
+ session["#{resource_name}_remember_me"] = remember_me
40
+ session["#{resource_name}_password_checked"] = true
41
+ session["#{resource_name}_return_to"] = return_to if return_to
42
+
43
+ redirect_to verify_fido_usf_path_for(resource_name)
44
+ return
45
+ end
46
+ end
47
+
48
+ def verify_fido_usf_path_for(resource_or_scope = nil)
49
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
50
+ send(:"new_#{scope}_fido_usf_authentication_path")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ Warden::Manager.after_authentication do |user, auth, options|
2
+ if user.respond_to?(:with_fido_usf_authentication?)
3
+ with_fido_usf_authentication = user.with_fido_usf_authentication?()
4
+ auth.session(options[:scope])[:with_fido_usf_authentication] = with_fido_usf_authentication
5
+ if with_fido_usf_authentication
6
+ auth.session(options[:scope])[:id] = user.id
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,15 @@
1
+ require 'devise_fido_usf/hooks/fido_usf_authenticatable'
2
+
3
+ module Devise
4
+ module Models
5
+ module FidoUsfAuthenticatable
6
+ extend ActiveSupport::Concern
7
+
8
+ # Does the user has a registered FIDO U2F device?
9
+ def with_fido_usf_authentication?()
10
+ FidoUsf::FidoUsfDevice.where(user: self).count()>0
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Devise
2
+ module Models
3
+ module FidoUsfRegisterable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_many :fido_usf_devices, class_name: 'FidoUsf::FidoUsfDevice', foreign_key: 'user_id', dependent: :destroy
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module DeviseFidoUsf
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include DeviseFidoUsf::Controllers::Helpers
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,15 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ def devise_fido_usf_registration(mapping, controllers)
4
+ resource :fido_usf_registration, :only => [:show, :new, :create, :destroy],
5
+ :path => mapping.path_names[:fido_usf_registration], :controller => controllers[:fido_usf_registrations] do
6
+ end
7
+ end
8
+ def devise_fido_usf_authentication(mapping, controllers)
9
+ resource :fido_usf_authentication, :only => [:new, :create],
10
+ :path => mapping.path_names[:fido_usf_authentication], :controller => controllers[:fido_usf_authentications] do
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,3 @@
1
+ module DeviseFidoUsf
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,59 @@
1
+ require 'rails/generators/base'
2
+
3
+ module DeviseFidoUsf
4
+ module Generators
5
+ MissingORMError = Class.new(Thor::Error)
6
+
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../../templates", __FILE__)
9
+
10
+ desc "Creates a FIDO U2F initializer for devise and copy locale files to your application."
11
+ class_option :orm
12
+
13
+ def copy_initializer
14
+ unless options[:orm]
15
+ raise MissingORMError, <<-ERROR.strip_heredoc
16
+ An ORM must be set to install Devise in your application.
17
+
18
+ Be sure to have an ORM like Active Record or Mongoid loaded in your
19
+ app or configure your own at `config/application.rb`.
20
+
21
+ config.generators do |g|
22
+ g.orm :your_orm_gem
23
+ end
24
+ ERROR
25
+ end
26
+
27
+ end
28
+
29
+ def add_application_helper
30
+ in_root do
31
+ inject_into_module "app/helpers/application_helper.rb", ApplicationHelper, application_helper_data
32
+ end
33
+ end
34
+
35
+ def copy_locale
36
+ copy_file "../../../config/locales/en.yml", "config/locales/fido_usf.en.yml"
37
+ end
38
+
39
+ def run_migration
40
+ invoke("devise_fido_usf:migrate", ["FidoUsfDevices"])
41
+ end
42
+
43
+ def show_readme
44
+ readme("README") if behavior == :invoke
45
+ end
46
+
47
+ def application_helper_data
48
+ <<RUBY
49
+
50
+ def u2f
51
+ # use base_url as app_id, e.g. 'http://localhost:3000'
52
+ @u2f ||= U2F::U2F.new(request.base_url)
53
+ end
54
+ RUBY
55
+ end
56
+ end
57
+ end
58
+ end
59
+