orthodox 0.2.4 → 0.3.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.
- checksums.yaml +4 -4
 - data/lib/generators/authentication/USAGE +14 -0
 - data/lib/generators/authentication/authentication_generator.rb +214 -0
 - data/lib/generators/authentication/templates/controllers/concerns/authentication.rb.erb +110 -0
 - data/lib/generators/authentication/templates/controllers/concerns/two_factor_authentication.rb +40 -0
 - data/lib/generators/authentication/templates/controllers/password_resets_controller.rb.erb +54 -0
 - data/lib/generators/authentication/templates/controllers/sessions_controller.rb.erb +36 -0
 - data/lib/generators/authentication/templates/controllers/tfa_sessions_controller.rb.erb +48 -0
 - data/lib/generators/authentication/templates/controllers/tfas_controller.rb.erb +38 -0
 - data/lib/generators/authentication/templates/helpers/otp_credentials_helper.rb +33 -0
 - data/lib/generators/authentication/templates/javascript/tfa_forms.js +19 -0
 - data/lib/generators/authentication/templates/models/concerns/authenticateable.rb +37 -0
 - data/lib/generators/authentication/templates/models/concerns/otpable.rb +26 -0
 - data/lib/generators/authentication/templates/models/concerns/password_resetable.rb +19 -0
 - data/lib/generators/authentication/templates/models/otp_credential.rb.erb +133 -0
 - data/lib/generators/authentication/templates/models/password_reset_token.rb +64 -0
 - data/lib/generators/authentication/templates/models/session.rb.erb +80 -0
 - data/lib/generators/authentication/templates/models/tfa_session.rb +77 -0
 - data/lib/generators/authentication/templates/spec/models/otp_credential_spec.rb +215 -0
 - data/lib/generators/authentication/templates/spec/models/password_reset_token_spec.rb +146 -0
 - data/lib/generators/authentication/templates/spec/models/session_spec.rb.erb +45 -0
 - data/lib/generators/authentication/templates/spec/models/tfa_session_spec.rb.erb +115 -0
 - data/lib/generators/authentication/templates/spec/support/authentication_helpers.rb +18 -0
 - data/lib/generators/authentication/templates/spec/support/factory_bot.rb +5 -0
 - data/lib/generators/authentication/templates/spec/system/authentication_spec.rb.erb +25 -0
 - data/lib/generators/authentication/templates/spec/system/password_resets_spec.rb.erb +73 -0
 - data/lib/generators/authentication/templates/spec/system/tfa_authentication_spec.rb.erb +38 -0
 - data/lib/generators/authentication/templates/views/mailers/password_reset_link.html.slim.erb +7 -0
 - data/lib/generators/authentication/templates/views/password_resets/edit.html.slim.erb +16 -0
 - data/lib/generators/authentication/templates/views/password_resets/new.html.slim.erb +12 -0
 - data/lib/generators/authentication/templates/views/sessions/new.html.slim.erb +21 -0
 - data/lib/generators/authentication/templates/views/tfa_sessions/new.html.slim.erb +26 -0
 - data/lib/generators/authentication/templates/views/tfas/show.html.slim.erb +9 -0
 - data/lib/generators/base_controller/USAGE +8 -0
 - data/lib/generators/base_controller/base_controller_generator.rb +22 -0
 - data/lib/generators/base_controller/templates/base_controller.rb.erb +7 -0
 - data/lib/generators/layout_helper/USAGE +8 -0
 - data/lib/generators/layout_helper/layout_helper_generator.rb +55 -0
 - data/lib/orthodox/version.rb +1 -1
 - metadata +39 -2
 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # Controller for managing sessions for <%= plural_class_name %>.
         
     | 
| 
      
 4 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 5 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 6 
     | 
    
         
            +
            class <%= plural_class_name %>::SessionsController < <%= plural_class_name %>::BaseController
         
     | 
| 
      
 7 
     | 
    
         
            +
              
         
     | 
| 
      
 8 
     | 
    
         
            +
              skip_before_action :authenticate_<%= singular_name %>
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              def new
         
     | 
| 
      
 11 
     | 
    
         
            +
                @<%= singular_name %>_session = <%= class_name %>Session.new(<%= singular_name %>_session_params)
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              def create
         
     | 
| 
      
 15 
     | 
    
         
            +
                @<%= singular_name %>_session = <%= class_name %>Session.new(<%= singular_name %>_session_params)
         
     | 
| 
      
 16 
     | 
    
         
            +
                if @<%= singular_name %>_session.valid?
         
     | 
| 
      
 17 
     | 
    
         
            +
                  sign_in(@<%= singular_name %>_session.<%= singular_name %>, as: :<%= singular_name %>)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  redirect_to(<%= plural_name %>_dashboard_url, notice: "Successfully signed in")
         
     | 
| 
      
 19 
     | 
    
         
            +
                else
         
     | 
| 
      
 20 
     | 
    
         
            +
                  render :new
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
              
         
     | 
| 
      
 24 
     | 
    
         
            +
              def destroy
         
     | 
| 
      
 25 
     | 
    
         
            +
                sign_out(:<%= singular_name %>)
         
     | 
| 
      
 26 
     | 
    
         
            +
                redirect_to root_url, notice: "Successfully signed out"
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              private
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              def <%= singular_name %>_session_params
         
     | 
| 
      
 32 
     | 
    
         
            +
                return {} unless params.key?(:<%= singular_name %>_session)
         
     | 
| 
      
 33 
     | 
    
         
            +
                params.require(:<%= singular_name %>_session).permit(:email, :password)
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Controller for managing two-factor sessions for <%= plural_class_name %>.
         
     | 
| 
      
 4 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 5 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 6 
     | 
    
         
            +
            class <%= plural_class_name %>::TfaSessionsController < <%= plural_class_name %>::BaseController
         
     | 
| 
      
 7 
     | 
    
         
            +
              
         
     | 
| 
      
 8 
     | 
    
         
            +
              skip_before_action :authenticate_<%= singular_name %>
         
     | 
| 
      
 9 
     | 
    
         
            +
              
         
     | 
| 
      
 10 
     | 
    
         
            +
              before_action :authenticate_<%= singular_name %>_without_tfa
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              before_action :ensure_<%= singular_name %>_has_active_tfa
         
     | 
| 
      
 13 
     | 
    
         
            +
              
         
     | 
| 
      
 14 
     | 
    
         
            +
              before_action :ensure_<%= singular_name %>_not_tfa_authenticated
         
     | 
| 
      
 15 
     | 
    
         
            +
              
         
     | 
| 
      
 16 
     | 
    
         
            +
              def new
         
     | 
| 
      
 17 
     | 
    
         
            +
                @tfa_session = TfaSession.new
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              def create
         
     | 
| 
      
 21 
     | 
    
         
            +
                @tfa_session = TfaSession.new(permitted_params.merge(record: current_<%= singular_name %>))
         
     | 
| 
      
 22 
     | 
    
         
            +
                if @tfa_session.valid?
         
     | 
| 
      
 23 
     | 
    
         
            +
                  current_<%= singular_name %>.otp_credential.consume_recovery_code!(permitted_params[:recovery_code])
         
     | 
| 
      
 24 
     | 
    
         
            +
                  sign_in(current_<%= singular_name %>, as: :<%= singular_name %>, tfa: true)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  redirect_to <%= singular_name %>_tfa_success_redirect_url
         
     | 
| 
      
 26 
     | 
    
         
            +
                else
         
     | 
| 
      
 27 
     | 
    
         
            +
                  render :new
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
              
         
     | 
| 
      
 31 
     | 
    
         
            +
              private
         
     | 
| 
      
 32 
     | 
    
         
            +
              
         
     | 
| 
      
 33 
     | 
    
         
            +
              def permitted_params
         
     | 
| 
      
 34 
     | 
    
         
            +
                params.require(:tfa_session).permit(:otp, :recovery_code)
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
              
         
     | 
| 
      
 37 
     | 
    
         
            +
              def ensure_<%= singular_name %>_has_active_tfa
         
     | 
| 
      
 38 
     | 
    
         
            +
                return if current_<%= singular_name %>.tfa?
         
     | 
| 
      
 39 
     | 
    
         
            +
                redirect_to <%= singular_name %>_tfa_success_redirect_url
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              def ensure_<%= singular_name %>_not_tfa_authenticated
         
     | 
| 
      
 43 
     | 
    
         
            +
                if current_<%= singular_name %>_tfa_authenticated?
         
     | 
| 
      
 44 
     | 
    
         
            +
                  redirect_to <%= singular_name %>_tfa_success_redirect_url    
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
              end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Controller for managing two-factor credentials for <%= plural_class_name %>.
         
     | 
| 
      
 4 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 5 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 6 
     | 
    
         
            +
            class <%= plural_class_name %>::TfasController < <%= plural_class_name %>::BaseController
         
     | 
| 
      
 7 
     | 
    
         
            +
              
         
     | 
| 
      
 8 
     | 
    
         
            +
              skip_before_action :authenticate_<%= singular_name %>
         
     | 
| 
      
 9 
     | 
    
         
            +
              
         
     | 
| 
      
 10 
     | 
    
         
            +
              before_action :authenticate_<%= singular_name %>_without_tfa
         
     | 
| 
      
 11 
     | 
    
         
            +
              
         
     | 
| 
      
 12 
     | 
    
         
            +
              ##
         
     | 
| 
      
 13 
     | 
    
         
            +
              # How long will we show the QRCode and recovery codes before they can no longer be
         
     | 
| 
      
 14 
     | 
    
         
            +
              # accessed?
         
     | 
| 
      
 15 
     | 
    
         
            +
              CAPTURE_TIME_ALLOWANCE = 15.seconds
         
     | 
| 
      
 16 
     | 
    
         
            +
              
         
     | 
| 
      
 17 
     | 
    
         
            +
              def create
         
     | 
| 
      
 18 
     | 
    
         
            +
                current_<%= singular_name %>.create_otp_credential!
         
     | 
| 
      
 19 
     | 
    
         
            +
                redirect_to(<%= plural_name %>_tfa_url, 
         
     | 
| 
      
 20 
     | 
    
         
            +
                            notice: "Successfully activated Two-Factor Authentication")
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              # This is where the <%= singular_name %> gets to see their recovery codes and QR Code.
         
     | 
| 
      
 24 
     | 
    
         
            +
              # After CAPTURE_TIME_ALLOWANCE they cannot re-visit this page
         
     | 
| 
      
 25 
     | 
    
         
            +
              def show
         
     | 
| 
      
 26 
     | 
    
         
            +
                if current_<%= singular_name %>.otp_credential.created_at < CAPTURE_TIME_ALLOWANCE.ago
         
     | 
| 
      
 27 
     | 
    
         
            +
                  redirect_to <%= plural_name %>_dashboard_url
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
              def destroy
         
     | 
| 
      
 32 
     | 
    
         
            +
                current_<%= singular_name %>.destroy_otp_credential
         
     | 
| 
      
 33 
     | 
    
         
            +
                redirect_to(<%= plural_name %>_dashboard_url, 
         
     | 
| 
      
 34 
     | 
    
         
            +
                            notice: "Successfully de-activated Two-Factor Authentication")
         
     | 
| 
      
 35 
     | 
    
         
            +
                
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,33 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # Helper for one-time-password authentication methods
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 6 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 7 
     | 
    
         
            +
            module OtpCredentialsHelper
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              require 'rqrcode'
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              # SVG code for a given OtpCredential. Use this to add a QR code to a page
         
     | 
| 
      
 12 
     | 
    
         
            +
              #
         
     | 
| 
      
 13 
     | 
    
         
            +
              # otp_credential - An OtpCredential to show the SVG for.
         
     | 
| 
      
 14 
     | 
    
         
            +
              #
         
     | 
| 
      
 15 
     | 
    
         
            +
              # Retuns String of valid HTML
         
     | 
| 
      
 16 
     | 
    
         
            +
              def svg_url_for_otp_credential(otp_credential)
         
     | 
| 
      
 17 
     | 
    
         
            +
                qrcode = qrcode(otp_credential)
         
     | 
| 
      
 18 
     | 
    
         
            +
                qrcode.as_svg({
         
     | 
| 
      
 19 
     | 
    
         
            +
                  offset: 0,
         
     | 
| 
      
 20 
     | 
    
         
            +
                  color: '000',
         
     | 
| 
      
 21 
     | 
    
         
            +
                  shape_rendering: 'crispEdges',
         
     | 
| 
      
 22 
     | 
    
         
            +
                  module_size: 3,
         
     | 
| 
      
 23 
     | 
    
         
            +
                  standalone: true      
         
     | 
| 
      
 24 
     | 
    
         
            +
                }).html_safe
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              private
         
     | 
| 
      
 28 
     | 
    
         
            +
              
         
     | 
| 
      
 29 
     | 
    
         
            +
              def qrcode(otp_credential)
         
     | 
| 
      
 30 
     | 
    
         
            +
                RQRCode::QRCode.new(otp_credential.url)
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
                
         
     | 
| 
      
 33 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            // Basic JS functionality for one-time-password form. Designed to be used with jQuery and 
         
     | 
| 
      
 2 
     | 
    
         
            +
            // Bootstrap.
         
     | 
| 
      
 3 
     | 
    
         
            +
            //
         
     | 
| 
      
 4 
     | 
    
         
            +
            // Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 5 
     | 
    
         
            +
            // (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 6 
     | 
    
         
            +
              
         
     | 
| 
      
 7 
     | 
    
         
            +
            // Initialize method called when jQuery ready
         
     | 
| 
      
 8 
     | 
    
         
            +
            function init() {
         
     | 
| 
      
 9 
     | 
    
         
            +
              $("body").on("click", ".js-tfa-link", onClick);
         
     | 
| 
      
 10 
     | 
    
         
            +
            }
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            // Callback when toggle links are clicked. Shows/hides form fields and links
         
     | 
| 
      
 13 
     | 
    
         
            +
            function onClick(e){
         
     | 
| 
      
 14 
     | 
    
         
            +
              e.preventDefault();
         
     | 
| 
      
 15 
     | 
    
         
            +
              $(".js-tfa-link").toggleClass("d-none");
         
     | 
| 
      
 16 
     | 
    
         
            +
              $(".js-tfa-field-group").toggleClass("d-none");
         
     | 
| 
      
 17 
     | 
    
         
            +
            }
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            jQuery(init);
         
     | 
| 
         @@ -0,0 +1,37 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # Model concern to provide shared behaviour for authenticating records.
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 6 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 7 
     | 
    
         
            +
            module Authenticateable
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              extend ActiveSupport::Concern
         
     | 
| 
      
 10 
     | 
    
         
            +
              
         
     | 
| 
      
 11 
     | 
    
         
            +
              MINIMUM_PASSWORD_LENGTH = 6
         
     | 
| 
      
 12 
     | 
    
         
            +
              
         
     | 
| 
      
 13 
     | 
    
         
            +
              MAXIMUM_PASSWORD_LENGTH = 128
         
     | 
| 
      
 14 
     | 
    
         
            +
              
         
     | 
| 
      
 15 
     | 
    
         
            +
              included do
         
     | 
| 
      
 16 
     | 
    
         
            +
                
         
     | 
| 
      
 17 
     | 
    
         
            +
                has_secure_password
         
     | 
| 
      
 18 
     | 
    
         
            +
                
         
     | 
| 
      
 19 
     | 
    
         
            +
                validates :email, email_format: { allow_blank: true }, presence: true
         
     | 
| 
      
 20 
     | 
    
         
            +
                
         
     | 
| 
      
 21 
     | 
    
         
            +
                validates :password, presence: { if: :validate_presence_of_password? }, 
         
     | 
| 
      
 22 
     | 
    
         
            +
                                     length: { minimum: MINIMUM_PASSWORD_LENGTH, 
         
     | 
| 
      
 23 
     | 
    
         
            +
                                               maximum: MAXIMUM_PASSWORD_LENGTH,
         
     | 
| 
      
 24 
     | 
    
         
            +
                                               allow_blank: true }
         
     | 
| 
      
 25 
     | 
    
         
            +
                
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
              
         
     | 
| 
      
 28 
     | 
    
         
            +
              private
         
     | 
| 
      
 29 
     | 
    
         
            +
              
         
     | 
| 
      
 30 
     | 
    
         
            +
              def validate_presence_of_password?
         
     | 
| 
      
 31 
     | 
    
         
            +
                new_record? || changes.include?("password")
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
              
         
     | 
| 
      
 34 
     | 
    
         
            +
              module ClassMethods
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
              
         
     | 
| 
      
 37 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Model concern to provide shared behaviour for two-factor auth (one-time password)
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 6 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 7 
     | 
    
         
            +
            module Otpable
         
     | 
| 
      
 8 
     | 
    
         
            +
              extend ActiveSupport::Concern
         
     | 
| 
      
 9 
     | 
    
         
            +
              
         
     | 
| 
      
 10 
     | 
    
         
            +
              included do
         
     | 
| 
      
 11 
     | 
    
         
            +
                
         
     | 
| 
      
 12 
     | 
    
         
            +
                has_one :otp_credential, as: :authable  
         
     | 
| 
      
 13 
     | 
    
         
            +
                
         
     | 
| 
      
 14 
     | 
    
         
            +
                delegate :valid_otp?, :valid_recovery_code?, to: :otp_credential
         
     | 
| 
      
 15 
     | 
    
         
            +
                
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
              
         
     | 
| 
      
 18 
     | 
    
         
            +
              def tfa?
         
     | 
| 
      
 19 
     | 
    
         
            +
                otp_credential.present?
         
     | 
| 
      
 20 
     | 
    
         
            +
              end
         
     | 
| 
      
 21 
     | 
    
         
            +
                
         
     | 
| 
      
 22 
     | 
    
         
            +
              def destroy_otp_credential
         
     | 
| 
      
 23 
     | 
    
         
            +
                otp_credential.destroy
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
              
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,19 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Model concern to provide shared behaviour for password resets
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 6 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 7 
     | 
    
         
            +
            module PasswordResetable
         
     | 
| 
      
 8 
     | 
    
         
            +
              
         
     | 
| 
      
 9 
     | 
    
         
            +
              extend ActiveSupport::Concern
         
     | 
| 
      
 10 
     | 
    
         
            +
              
         
     | 
| 
      
 11 
     | 
    
         
            +
              included do
         
     | 
| 
      
 12 
     | 
    
         
            +
                has_one :password_reset_token, as: :resetable, dependent: :destroy
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
              
         
     | 
| 
      
 15 
     | 
    
         
            +
              def destroy_password_reset_token
         
     | 
| 
      
 16 
     | 
    
         
            +
                password_reset_token.try(:destroy)
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
              
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,133 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
            # 
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Model for managing one-time password credentials for a given Member
         
     | 
| 
      
 4 
     | 
    
         
            +
            #
         
     | 
| 
      
 5 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 6 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 7 
     | 
    
         
            +
            #
         
     | 
| 
      
 8 
     | 
    
         
            +
            # == Schema Information
         
     | 
| 
      
 9 
     | 
    
         
            +
            #
         
     | 
| 
      
 10 
     | 
    
         
            +
            # Table name: otp_credentials
         
     | 
| 
      
 11 
     | 
    
         
            +
            #
         
     | 
| 
      
 12 
     | 
    
         
            +
            #  id             :bigint           not null, primary key
         
     | 
| 
      
 13 
     | 
    
         
            +
            #  authable_type  :string           not null
         
     | 
| 
      
 14 
     | 
    
         
            +
            #  last_used_at   :datetime
         
     | 
| 
      
 15 
     | 
    
         
            +
            #  recovery_codes :json
         
     | 
| 
      
 16 
     | 
    
         
            +
            #  secret         :string(32)
         
     | 
| 
      
 17 
     | 
    
         
            +
            #  authable_id    :bigint           not null
         
     | 
| 
      
 18 
     | 
    
         
            +
            #
         
     | 
| 
      
 19 
     | 
    
         
            +
            # Indexes
         
     | 
| 
      
 20 
     | 
    
         
            +
            #
         
     | 
| 
      
 21 
     | 
    
         
            +
            #  index_otp_credentials_on_authable_type_and_authable_id  (authable_type,authable_id)
         
     | 
| 
      
 22 
     | 
    
         
            +
            #
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            class OtpCredential < ApplicationRecord
         
     | 
| 
      
 25 
     | 
    
         
            +
              
         
     | 
| 
      
 26 
     | 
    
         
            +
              ##
         
     | 
| 
      
 27 
     | 
    
         
            +
              # How much of a 'grace' period should we give the user, after which we will accept
         
     | 
| 
      
 28 
     | 
    
         
            +
              # expired OTPs. Time in seconds.
         
     | 
| 
      
 29 
     | 
    
         
            +
              DRIFT_ALLOWANCE = 15
         
     | 
| 
      
 30 
     | 
    
         
            +
              
         
     | 
| 
      
 31 
     | 
    
         
            +
              # ==============
         
     | 
| 
      
 32 
     | 
    
         
            +
              # = Attributes =
         
     | 
| 
      
 33 
     | 
    
         
            +
              # ==============
         
     | 
| 
      
 34 
     | 
    
         
            +
              
         
     | 
| 
      
 35 
     | 
    
         
            +
              attr_readonly :authable_type, :authable_id
         
     | 
| 
      
 36 
     | 
    
         
            +
                
         
     | 
| 
      
 37 
     | 
    
         
            +
              serialize :recovery_codes
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              # ================
         
     | 
| 
      
 40 
     | 
    
         
            +
              # = Associations =
         
     | 
| 
      
 41 
     | 
    
         
            +
              # ================
         
     | 
| 
      
 42 
     | 
    
         
            +
              
         
     | 
| 
      
 43 
     | 
    
         
            +
              belongs_to :authable, polymorphic: true
         
     | 
| 
      
 44 
     | 
    
         
            +
              
         
     | 
| 
      
 45 
     | 
    
         
            +
              
         
     | 
| 
      
 46 
     | 
    
         
            +
              # =============
         
     | 
| 
      
 47 
     | 
    
         
            +
              # = Callbacks =
         
     | 
| 
      
 48 
     | 
    
         
            +
              # =============
         
     | 
| 
      
 49 
     | 
    
         
            +
              
         
     | 
| 
      
 50 
     | 
    
         
            +
              before_create :set_secret
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              before_create :set_last_used_at
         
     | 
| 
      
 53 
     | 
    
         
            +
              
         
     | 
| 
      
 54 
     | 
    
         
            +
              before_create :set_recovery_codes
         
     | 
| 
      
 55 
     | 
    
         
            +
                
         
     | 
| 
      
 56 
     | 
    
         
            +
                
         
     | 
| 
      
 57 
     | 
    
         
            +
              # ===========================
         
     | 
| 
      
 58 
     | 
    
         
            +
              # = Public instance methods =
         
     | 
| 
      
 59 
     | 
    
         
            +
              # ===========================
         
     | 
| 
      
 60 
     | 
    
         
            +
              
         
     | 
| 
      
 61 
     | 
    
         
            +
              # URL for generating QR code for this OTP.
         
     | 
| 
      
 62 
     | 
    
         
            +
              #
         
     | 
| 
      
 63 
     | 
    
         
            +
              # Returns String
         
     | 
| 
      
 64 
     | 
    
         
            +
              def url
         
     | 
| 
      
 65 
     | 
    
         
            +
                totp.provisioning_uri(authable.email)
         
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
              
         
     | 
| 
      
 68 
     | 
    
         
            +
              # Test the given code against the expected current value.
         
     | 
| 
      
 69 
     | 
    
         
            +
              #
         
     | 
| 
      
 70 
     | 
    
         
            +
              # Returns Integer (Timestamp)
         
     | 
| 
      
 71 
     | 
    
         
            +
              # Returns nil
         
     | 
| 
      
 72 
     | 
    
         
            +
              def valid_otp?(test_value)
         
     | 
| 
      
 73 
     | 
    
         
            +
                if result = totp.verify(test_value, 
         
     | 
| 
      
 74 
     | 
    
         
            +
                                        after: last_used_at, 
         
     | 
| 
      
 75 
     | 
    
         
            +
                                        drift_behind: DRIFT_ALLOWANCE)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  touch(:last_used_at)
         
     | 
| 
      
 77 
     | 
    
         
            +
                end
         
     | 
| 
      
 78 
     | 
    
         
            +
                result
         
     | 
| 
      
 79 
     | 
    
         
            +
              end
         
     | 
| 
      
 80 
     | 
    
         
            +
                
         
     | 
| 
      
 81 
     | 
    
         
            +
              # Test the given recovery code against the stored recovery_codes 
         
     | 
| 
      
 82 
     | 
    
         
            +
              #
         
     | 
| 
      
 83 
     | 
    
         
            +
              # Returns Boolean
         
     | 
| 
      
 84 
     | 
    
         
            +
              def valid_recovery_code?(test_value)
         
     | 
| 
      
 85 
     | 
    
         
            +
                test_value.to_s.in?(recovery_codes)
         
     | 
| 
      
 86 
     | 
    
         
            +
              end
         
     | 
| 
      
 87 
     | 
    
         
            +
              
         
     | 
| 
      
 88 
     | 
    
         
            +
              # Removes a used recovery code from the recovery_codes list.
         
     | 
| 
      
 89 
     | 
    
         
            +
              #
         
     | 
| 
      
 90 
     | 
    
         
            +
              # Returns Boolean
         
     | 
| 
      
 91 
     | 
    
         
            +
              def consume_recovery_code!(recovery_code)
         
     | 
| 
      
 92 
     | 
    
         
            +
                array = recovery_codes
         
     | 
| 
      
 93 
     | 
    
         
            +
                array.delete(recovery_code)
         
     | 
| 
      
 94 
     | 
    
         
            +
                update_attribute(:recovery_codes, array)
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
              private
         
     | 
| 
      
 98 
     | 
    
         
            +
              
         
     | 
| 
      
 99 
     | 
    
         
            +
              # The expected current OTP value. This shouldn't need to be required in production
         
     | 
| 
      
 100 
     | 
    
         
            +
              #
         
     | 
| 
      
 101 
     | 
    
         
            +
              # Returns String
         
     | 
| 
      
 102 
     | 
    
         
            +
              def current_otp
         
     | 
| 
      
 103 
     | 
    
         
            +
                totp.now
         
     | 
| 
      
 104 
     | 
    
         
            +
              end
         
     | 
| 
      
 105 
     | 
    
         
            +
              
         
     | 
| 
      
 106 
     | 
    
         
            +
              # Set the secret value to a random base 32 String
         
     | 
| 
      
 107 
     | 
    
         
            +
              #
         
     | 
| 
      
 108 
     | 
    
         
            +
              # Returns String
         
     | 
| 
      
 109 
     | 
    
         
            +
              def set_secret
         
     | 
| 
      
 110 
     | 
    
         
            +
                self.secret = ROTP::Base32.random
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
              
         
     | 
| 
      
 113 
     | 
    
         
            +
              # Ensure the last_used_at time is always present and a past datetime.
         
     | 
| 
      
 114 
     | 
    
         
            +
              def set_last_used_at
         
     | 
| 
      
 115 
     | 
    
         
            +
                self.last_used_at = 5.minutes.ago
         
     | 
| 
      
 116 
     | 
    
         
            +
              end
         
     | 
| 
      
 117 
     | 
    
         
            +
                
         
     | 
| 
      
 118 
     | 
    
         
            +
              def set_recovery_codes
         
     | 
| 
      
 119 
     | 
    
         
            +
                self.recovery_codes = 10.times.map { generate_recovery_code }
         
     | 
| 
      
 120 
     | 
    
         
            +
              end
         
     | 
| 
      
 121 
     | 
    
         
            +
              
         
     | 
| 
      
 122 
     | 
    
         
            +
              def generate_recovery_code
         
     | 
| 
      
 123 
     | 
    
         
            +
                "#{SecureRandom.hex(3)[0..4]}-#{SecureRandom.hex(3)[0..4]}"
         
     | 
| 
      
 124 
     | 
    
         
            +
              end
         
     | 
| 
      
 125 
     | 
    
         
            +
              
         
     | 
| 
      
 126 
     | 
    
         
            +
              # An instance of the TOTP to test codes against.
         
     | 
| 
      
 127 
     | 
    
         
            +
              #
         
     | 
| 
      
 128 
     | 
    
         
            +
              # Returns ROTP::TOTP
         
     | 
| 
      
 129 
     | 
    
         
            +
              def totp
         
     | 
| 
      
 130 
     | 
    
         
            +
                @totp ||= ROTP::TOTP.new(secret, issuer: "<%= Rails.application.class.module_parent.name %>")
         
     | 
| 
      
 131 
     | 
    
         
            +
              end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
         
     | 
| 
      
 133 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,64 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            # 
         
     | 
| 
      
 4 
     | 
    
         
            +
            # Model for managing password reset tokens
         
     | 
| 
      
 5 
     | 
    
         
            +
            #
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox)
         
     | 
| 
      
 7 
     | 
    
         
            +
            # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. 
         
     | 
| 
      
 8 
     | 
    
         
            +
            #
         
     | 
| 
      
 9 
     | 
    
         
            +
            # == Schema Information
         
     | 
| 
      
 10 
     | 
    
         
            +
            #
         
     | 
| 
      
 11 
     | 
    
         
            +
            # Table name: password_reset_tokens
         
     | 
| 
      
 12 
     | 
    
         
            +
            #
         
     | 
| 
      
 13 
     | 
    
         
            +
            #  id             :bigint           not null, primary key
         
     | 
| 
      
 14 
     | 
    
         
            +
            #  expires_at     :datetime
         
     | 
| 
      
 15 
     | 
    
         
            +
            #  resetable_type :string           not null
         
     | 
| 
      
 16 
     | 
    
         
            +
            #  secret         :string
         
     | 
| 
      
 17 
     | 
    
         
            +
            #  created_at     :datetime         not null
         
     | 
| 
      
 18 
     | 
    
         
            +
            #  updated_at     :datetime         not null
         
     | 
| 
      
 19 
     | 
    
         
            +
            #  resetable_id   :bigint           not null
         
     | 
| 
      
 20 
     | 
    
         
            +
            #
         
     | 
| 
      
 21 
     | 
    
         
            +
            # Indexes
         
     | 
| 
      
 22 
     | 
    
         
            +
            #
         
     | 
| 
      
 23 
     | 
    
         
            +
            #  index_password_reset_tokens_on_expires_at                       (expires_at)
         
     | 
| 
      
 24 
     | 
    
         
            +
            #  index_password_reset_tokens_on_resetable_type_and_resetable_id  (resetable_type,resetable_id)
         
     | 
| 
      
 25 
     | 
    
         
            +
            #  index_password_reset_tokens_on_secret                           (secret) UNIQUE
         
     | 
| 
      
 26 
     | 
    
         
            +
            #
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            class PasswordResetToken < ApplicationRecord
         
     | 
| 
      
 29 
     | 
    
         
            +
              
         
     | 
| 
      
 30 
     | 
    
         
            +
              # =============
         
     | 
| 
      
 31 
     | 
    
         
            +
              # = Constants =
         
     | 
| 
      
 32 
     | 
    
         
            +
              # =============
         
     | 
| 
      
 33 
     | 
    
         
            +
              
         
     | 
| 
      
 34 
     | 
    
         
            +
              ##
         
     | 
| 
      
 35 
     | 
    
         
            +
              # How long should password reset links be valid for?
         
     | 
| 
      
 36 
     | 
    
         
            +
              EXPIRES_AFTER = 15.minutes
         
     | 
| 
      
 37 
     | 
    
         
            +
              
         
     | 
| 
      
 38 
     | 
    
         
            +
              # ================
         
     | 
| 
      
 39 
     | 
    
         
            +
              # = Associations =
         
     | 
| 
      
 40 
     | 
    
         
            +
              # ================
         
     | 
| 
      
 41 
     | 
    
         
            +
              
         
     | 
| 
      
 42 
     | 
    
         
            +
              belongs_to :resetable, polymorphic: true
         
     | 
| 
      
 43 
     | 
    
         
            +
              
         
     | 
| 
      
 44 
     | 
    
         
            +
              # ==============
         
     | 
| 
      
 45 
     | 
    
         
            +
              # = Attributes =
         
     | 
| 
      
 46 
     | 
    
         
            +
              # ==============
         
     | 
| 
      
 47 
     | 
    
         
            +
              
         
     | 
| 
      
 48 
     | 
    
         
            +
              has_secure_token :secret
         
     | 
| 
      
 49 
     | 
    
         
            +
              
         
     | 
| 
      
 50 
     | 
    
         
            +
              attr_readonly :resetable_type, :resetable_id, :expires_at, :secret
         
     | 
| 
      
 51 
     | 
    
         
            +
              
         
     | 
| 
      
 52 
     | 
    
         
            +
              before_create :set_expires_at
         
     | 
| 
      
 53 
     | 
    
         
            +
              
         
     | 
| 
      
 54 
     | 
    
         
            +
              def expired?
         
     | 
| 
      
 55 
     | 
    
         
            +
                expires_at <= Time.current
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
              
         
     | 
| 
      
 58 
     | 
    
         
            +
              private
         
     | 
| 
      
 59 
     | 
    
         
            +
              
         
     | 
| 
      
 60 
     | 
    
         
            +
              def set_expires_at
         
     | 
| 
      
 61 
     | 
    
         
            +
                self.expires_at = EXPIRES_AFTER.from_now
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
              
         
     | 
| 
      
 64 
     | 
    
         
            +
            end
         
     |