devise_twilio_two_factor 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +59 -0
  9. data/Rakefile +8 -0
  10. data/app/controllers/devise/twilio_two_factor_controller.rb +83 -0
  11. data/app/views/devise/twilio_two_factor/show.html.erb +10 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/config/locales/de.yml +8 -0
  15. data/config/locales/en.yml +8 -0
  16. data/config/locales/es.yml +8 -0
  17. data/config/locales/fr.yml +8 -0
  18. data/config/locales/ru.yml +8 -0
  19. data/devise_twilio_two_factor.gemspec +48 -0
  20. data/lib/devise_twilio_two_factor/callbacks/twilio_two_factor_authenticatable.rb +17 -0
  21. data/lib/devise_twilio_two_factor/callbacks.rb +1 -0
  22. data/lib/devise_twilio_two_factor/controllers/helpers.rb +54 -0
  23. data/lib/devise_twilio_two_factor/controllers.rb +1 -0
  24. data/lib/devise_twilio_two_factor/models/twilio_two_factor_authenticatable.rb +44 -0
  25. data/lib/devise_twilio_two_factor/models.rb +1 -0
  26. data/lib/devise_twilio_two_factor/rails.rb +7 -0
  27. data/lib/devise_twilio_two_factor/routes.rb +11 -0
  28. data/lib/devise_twilio_two_factor/services/twilio_two_factor_client.rb +41 -0
  29. data/lib/devise_twilio_two_factor/services.rb +1 -0
  30. data/lib/devise_twilio_two_factor/version.rb +5 -0
  31. data/lib/devise_twilio_two_factor.rb +46 -0
  32. data/lib/generators/devise_twilio_two_factor/devise_twilio_two_factor_generator.rb +46 -0
  33. data/sig/devise_twilio_two_factor.rbs +4 -0
  34. data/spec/models/twilio_two_factor_authenticatable_spec.rb +89 -0
  35. data/spec/services/twilio_two_factor_auth_client_spec.rb +78 -0
  36. data/spec/spec_helper.rb +17 -0
  37. metadata +180 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b1ecc275a6e5ae2ca9c0cad512fc307cdf6bc46e374f231266f700a1a09fe71
4
+ data.tar.gz: 774d26123c2f820cab0babc3a049ebe78df4572cb386a9357dd1a8d6b14f62b6
5
+ SHA512:
6
+ metadata.gz: 722b23fa02287db9b63ee3bfef7cb3326d04d866befe62af8456a1c3d6df2ff8f20469de862e2fd93e76f5064896ff60d6f968c642f9d98ded90359fb29658e3
7
+ data.tar.gz: 44e29e36e736438b7d648085698d0e11fa710edc052be66ccf7b0e4e052783ed45a0008888904951addb41ea14e345c0f2f8f01cc27bae118d98de6cf950e966
@@ -0,0 +1,27 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ pull_request:
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ name: Ruby ${{ matrix.ruby }}
14
+ strategy:
15
+ matrix:
16
+ ruby:
17
+ - '3.1.2'
18
+
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby }}
25
+ bundler-cache: true
26
+ - name: Run the default task
27
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-03-15
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in devise_twilio_two_factor.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 TODO: Write your name
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,59 @@
1
+ # Devise Twilio Two Factor
2
+
3
+ The "Devise Twilio Two-Factor Authentication Gem" is an integration solution that brings together the robust security features of the Devise gem and the reliable functionality of Twilio APIs. With this solution, incorporating two-factor authentication for user authentication in your application has never been easier. By leveraging the power of Devise and Twilio together, your application can offer enhanced security measures to your users, reducing the likelihood of unauthorized access and potential data breaches.
4
+
5
+ ## Installation
6
+
7
+ Add devise_twilio_two_factor to your Gemfile with:
8
+
9
+ ```ruby
10
+ # Gemfile
11
+
12
+ gem 'devise_twilio_two_factor', '~> 0.1.0'
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ To integrate the Devise Twilio Two-Factor Authentication Gem, you'll need:
18
+
19
+ - The [Devise gem](https://github.com/heartcombo/devise), set up according to their instructions.
20
+ - A [Twilio](https://github.com/heartcombo/devise) account with the account_sid and auth token from the console.
21
+ - A Verify Service with its ID.
22
+
23
+ Add them to your devise.rb
24
+ ```ruby
25
+ config.twilio_account_sid = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
26
+ config.twilio_auth_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
27
+ config.twilio_verify_service_sid = "VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
28
+ ```
29
+
30
+ From here, the generator should get you the rest of the way (you can skip the rest of the section):
31
+ ```bash
32
+ ./bin/rails generate devise_two_factor MODEL
33
+ ```
34
+
35
+ To add two-factor authentication to a model, simply add the Devise Twilio Two-Factor Authenticatable module and specify two options:
36
+
37
+ 1) The otp_destination option should represent the field containing the phone number or email address where the OTP code will be sent.
38
+ 2) The communication_type option should be set to either "sms" or "email" depending on the desired mode of communication.
39
+
40
+ Ex. for a user with a phone field -> user.phone = '+18001234567'
41
+ ```ruby
42
+ devise :twilio_two_factor_authenticatable, otp_destination: 'phone', communication_type: "sms"
43
+ ```
44
+
45
+ lastly, just create a migration to add `otp_required_for_login:boolean` to the table of the resource you wish to add 2fa to.
46
+
47
+ ## Development
48
+
49
+ 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.
50
+
51
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
52
+
53
+ ## Contributing
54
+
55
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/devise_twilio_two_factor.
56
+
57
+ ## License
58
+
59
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,83 @@
1
+ require 'devise/version'
2
+
3
+ class Devise::TwilioTwoFactorController < DeviseController
4
+ prepend_before_action :authenticate_scope!
5
+ before_action :prepare_and_validate, :handle_two_factor_authentication
6
+
7
+ def authenticate_scope!
8
+ self.resource = send("current_#{resource_name}")
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def update
15
+ render :show and return if params[:code].nil?
16
+
17
+ if resource.verify_otp_code(params[:code])
18
+ after_two_factor_success_for(resource)
19
+ else
20
+ after_two_factor_fail_for(resource)
21
+ end
22
+ end
23
+
24
+ def resend_code
25
+ resource.send_otp_code
26
+ redirect_to send("#{resource_name}_twilio_two_factor_path"), notice: I18n.t('devise.twilio_two_factor.code_has_been_sent')
27
+ end
28
+
29
+ private
30
+
31
+ def after_two_factor_success_for(resource)
32
+ set_remember_two_factor_cookie(resource)
33
+
34
+ warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
35
+
36
+ if respond_to?(:bypass_sign_in)
37
+ bypass_sign_in(resource, scope: resource_name)
38
+ else
39
+ sign_in(resource_name, resource, bypass: true)
40
+ end
41
+ set_flash_message :notice, :success
42
+ resource.update_attribute(:failed_attempts, 0)
43
+
44
+ redirect_to after_two_factor_success_path_for(resource)
45
+ end
46
+
47
+ def after_two_factor_fail_for(resource)
48
+ resource.failed_attempts += 1
49
+ resource.save
50
+ set_flash_message :alert, :attempt_failed, now: true
51
+
52
+ if resource.login_attempts_exceeded?
53
+ sign_out(resource)
54
+ redirect_to :root
55
+ else
56
+ render :show
57
+ end
58
+ end
59
+
60
+ def set_remember_two_factor_cookie(resource)
61
+ expires_seconds = resource.class.remember_otp_session_for_seconds
62
+
63
+ if expires_seconds && expires_seconds > 0
64
+ cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
65
+ value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
66
+ expires: expires_seconds.seconds.from_now
67
+ }
68
+ end
69
+ end
70
+
71
+ def after_two_factor_success_path_for(resource)
72
+ stored_location_for(resource_name) || :root
73
+ end
74
+
75
+ def prepare_and_validate
76
+ redirect_to :root and return if resource.nil?
77
+
78
+ if resource.login_attempts_exceeded?
79
+ sign_out(resource)
80
+ redirect_to :root
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,10 @@
1
+ <h2>Enter OTP Code</h2>
2
+
3
+ <p><%= flash[:notice] %></p>
4
+
5
+ <%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
6
+ <%= text_field_tag :code, '', autofocus: true %>
7
+ <%= submit_tag "Submit" %>
8
+ <% end %>
9
+ <%= link_to "Resend Code", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
10
+ <%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %>
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "devise_twilio_two_factor"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ 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
@@ -0,0 +1,8 @@
1
+ de:
2
+ devise:
3
+ twilio_two_factor:
4
+ success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich."
5
+ attempt_failed: "Authentifizierungsversuch fehlgeschlagen."
6
+ max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben."
7
+ contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren."
8
+ code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt."
@@ -0,0 +1,8 @@
1
+ en:
2
+ devise:
3
+ twilio_two_factor:
4
+ success: "Two factor authentication successful."
5
+ attempt_failed: "Attempt failed."
6
+ max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
7
+ contact_administrator: "Please contact your system administrator."
8
+ code_has_been_sent: "Your authentication code has been sent."
@@ -0,0 +1,8 @@
1
+ es:
2
+ devise:
3
+ twilio_two_factor:
4
+ success: "Autenticación multi-factor realizada exitosamente."
5
+ attempt_failed: "La autenticación ha fallado."
6
+ max_login_attempts_reached: "Has llegado al límite de intentos fallidos, acceso denegado."
7
+ contact_administrator: "Contacte a su administrador de sistema."
8
+ code_has_been_sent: "El código de autenticación ha sido enviado."
@@ -0,0 +1,8 @@
1
+ fr:
2
+ devise:
3
+ twilio_two_factor:
4
+ success: "Validation en deux étapes effectuée avec succès."
5
+ attempt_failed: "La connexion a échoué."
6
+ max_login_attempts_reached: "Limite de tentatives atteinte, accès refusé."
7
+ contact_administrator: "Merci de contacter votre administrateur système."
8
+ code_has_been_sent: "Votre code de validation envoyé."
@@ -0,0 +1,8 @@
1
+ ru:
2
+ devise:
3
+ twilio_two_factor:
4
+ success: "Двухфакторная авторизация успешно пройдена."
5
+ attempt_failed: "Неверный код."
6
+ max_login_attempts_reached: "Доступ заблокирован. Превышено число попыток авторизации"
7
+ contact_administrator: "Пожалуйста, свяжитесь с системным администратором."
8
+ code_has_been_sent: "Ваш персональный код был отправлен."
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/devise_twilio_two_factor/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'devise_twilio_two_factor'
7
+ spec.version = DeviseTwilioTwoFactor::VERSION
8
+ spec.authors = ['John Livingston']
9
+ spec.email = 'jclivingston316@gmail.com'
10
+
11
+
12
+ spec.platform = Gem::Platform::RUBY
13
+ spec.licenses = ['MIT']
14
+ spec.summary = 'Devise Twilio Verify Two Factor Authentication'
15
+ spec.homepage = 'https://github.com/jliv316/devise-twilio-two-factor'
16
+ spec.description = 'Devise Twilio Verify Two Factor Authentication'
17
+ spec.required_ruby_version = '>= 2.6.0'
18
+
19
+ # spec.metadata['allowed_push_host'] = "TODO: Set to your gem server 'https://example.com'"
20
+
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['source_code_uri'] = 'https://github.com/Jliv316/devise_twilio_two_factor'
23
+ # spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
30
+ end
31
+ end
32
+ spec.bindir = 'exe'
33
+ spec.files = `git ls-files`.split("\n")
34
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ['lib']
37
+
38
+
39
+
40
+ spec.add_runtime_dependency 'railties', '>= 4.1.0'
41
+ spec.add_runtime_dependency 'devise', '~> 4.8.1'
42
+ spec.add_runtime_dependency 'twilio-ruby', '~> 5.66.0'
43
+
44
+ spec.add_development_dependency 'bundler', '> 1.0'
45
+ spec.add_development_dependency 'rspec', '> 3'
46
+ spec.add_development_dependency 'activemodel'
47
+ spec.add_development_dependency 'pry'
48
+ end
@@ -0,0 +1,17 @@
1
+ Warden::Manager.after_authentication do |user, auth, options|
2
+ if auth.env["action_dispatch.cookies"]
3
+ expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}"
4
+ actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
5
+ bypass_by_cookie = actual_cookie_value == expected_cookie_value
6
+ end
7
+
8
+ if user.respond_to?(:otp_required_for_login) && !bypass_by_cookie
9
+ if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
10
+ user.send_otp_code if user.send_new_otp_after_login?
11
+ end
12
+ end
13
+ end
14
+
15
+ Warden::Manager.before_logout do |user, auth, _options|
16
+ auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
17
+ end
@@ -0,0 +1 @@
1
+ require_relative "callbacks/twilio_two_factor_authenticatable"
@@ -0,0 +1,54 @@
1
+ module DeviseTwilioTwoFactor
2
+ module Controllers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :handle_two_factor_authentication
8
+ end
9
+
10
+ private
11
+
12
+ def handle_two_factor_authentication
13
+ unless devise_controller?
14
+ Devise.mappings.keys.flatten.any? do |scope|
15
+ if signed_in?(scope) && warden.session(scope)[TwoFactorAuthentication::NEED_AUTHENTICATION]
16
+ handle_failed_second_factor(scope)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def handle_failed_second_factor(scope)
23
+ if request.format.present?
24
+ if request.format.html?
25
+ session["#{scope}_return_to"] = request.original_fullpath if request.get?
26
+ redirect_to twilio_two_factor_path_for(scope)
27
+ elsif request.format.json?
28
+ session["#{scope}_return_to"] = root_path(format: :html)
29
+ render json: { redirect_to: twilio_two_factor_path_for(scope) }, status: :unauthorized
30
+ end
31
+ else
32
+ head :unauthorized
33
+ end
34
+ end
35
+
36
+ def twilio_two_factor_path_for(resource_or_scope = nil)
37
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
38
+ change_path = "#{scope}_twilio_two_factor_path"
39
+ send(change_path)
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+
46
+ module Devise
47
+ module Controllers
48
+ module Helpers
49
+ def is_fully_authenticated?
50
+ !session["warden.user.user.session"].try(:[], TwoFactorAuthentication::NEED_AUTHENTICATION)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1 @@
1
+ require_relative "controllers/helpers.rb"
@@ -0,0 +1,44 @@
1
+ module Devise
2
+ module Models
3
+ module TwilioTwoFactorAuthenticatable
4
+ extend ActiveSupport::Concern
5
+
6
+ def send_otp_code
7
+ twilio_client.send_code
8
+ end
9
+
10
+ def verify_otp_code(code)
11
+ twilio_client.verify_code(code)
12
+ end
13
+
14
+ def login_attempts_exceeded?
15
+ self.failed_attempts.to_i >= Devise.maximum_attempts
16
+ end
17
+
18
+ def need_two_factor_authentication?(request)
19
+ self.otp_required_for_login
20
+ end
21
+
22
+ def send_new_otp_after_login?
23
+ self.otp_required_for_login
24
+ end
25
+
26
+ private def twilio_client
27
+ @twilio_client ||= TwilioTwoFactorAuthClient.new(self)
28
+ end
29
+
30
+ protected
31
+ module ClassMethods
32
+ Devise::Models.config(self,
33
+ :otp_code_length,
34
+ :otp_destination,
35
+ :communication_type,
36
+ :remember_otp_session_for_seconds,
37
+ :second_factor_resource_id,
38
+ :twilio_verify_service_sid,
39
+ :twilio_auth_token,
40
+ :twilio_account_sid)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1 @@
1
+ require_relative "models/twilio_two_factor_authenticatable"
@@ -0,0 +1,7 @@
1
+ module DeviseTwilioTwoFactor
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include DeviseTwilioTwoFactor::Controllers::Helpers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ protected
4
+
5
+ def devise_twilio_two_factor(mapping, controllers)
6
+ resource :twilio_two_factor, :only => [:show, :update, :resend_code], :path => mapping.path_names[:twilio_two_factor], :controller => controllers[:twilio_two_factor] do
7
+ collection { get "resend_code" }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ class TwilioTwoFactorAuthClient
2
+ STATUS_PENDING = "pending"
3
+ STATUS_APPROVED = "approved"
4
+
5
+ def initialize(resource)
6
+ @resource = resource
7
+ @client = Twilio::REST::Client.new(resource.class.twilio_account_sid, resource.class.twilio_auth_token)
8
+ end
9
+
10
+ def send_code
11
+ begin
12
+ response = @client.verify
13
+ .v2
14
+ .services(@resource.class.twilio_verify_service_sid)
15
+ .verifications
16
+ .create(to: @resource.send(@resource.class.otp_destination), channel: @resource.class.communication_type)
17
+
18
+ return true if response.status == STATUS_PENDING
19
+ return false
20
+ rescue Twilio::REST::RestError => e
21
+ puts e.message
22
+ return false
23
+ end
24
+ end
25
+
26
+ def verify_code(code)
27
+ begin
28
+ response = @client.verify
29
+ .v2
30
+ .services(@resource.class.twilio_verify_service_sid)
31
+ .verification_checks
32
+ .create(to: @resource.send(@resource.class.otp_destination), code: code)
33
+ return true if response.status == STATUS_APPROVED
34
+ return false
35
+ rescue Twilio::REST::RestError => e
36
+ puts e.message
37
+ return false
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1 @@
1
+ require_relative "services/twilio_two_factor_client"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseTwilioTwoFactor
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require "rubygems"
3
+ require "devise"
4
+ require "twilio-ruby"
5
+ require_relative "devise_twilio_two_factor/version"
6
+ require_relative "devise_twilio_two_factor/models"
7
+ require_relative "devise_twilio_two_factor/callbacks"
8
+ require_relative "devise_twilio_two_factor/services"
9
+ require_relative "devise_twilio_two_factor/rails"
10
+ require_relative "devise_twilio_two_factor/routes"
11
+ require_relative "devise_twilio_two_factor/controllers"
12
+
13
+ module Devise
14
+ mattr_accessor :otp_code_length
15
+ @@otp_code_length = 6
16
+
17
+ mattr_accessor :twilio_account_sid
18
+ @@twilio_account_sid = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
19
+
20
+ mattr_accessor :twilio_auth_token
21
+ @@twilio_auth_token = "token"
22
+
23
+ mattr_accessor :twilio_verify_service_sid
24
+ @@twilio_verify_service_sid = "VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
25
+
26
+ mattr_accessor :second_factor_resource_id
27
+ @@second_factor_resource_id = "id"
28
+
29
+ mattr_accessor :remember_otp_session_for_seconds
30
+ @@remember_otp_session_for_seconds = 30.minutes
31
+
32
+ mattr_accessor :delete_cookie_on_logout
33
+ @@delete_cookie_on_logout = true
34
+ end
35
+
36
+ module TwoFactorAuthentication
37
+ NEED_AUTHENTICATION = "otp_required_for_login"
38
+ REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
39
+
40
+ module Controllers
41
+ autoload :Helpers, "devise_twilio_two_factor/controllers/helpers"
42
+ end
43
+ end
44
+
45
+ Devise.add_module(:twilio_two_factor_authenticatable, :route => :twilio_two_factor,
46
+ :controller => :twilio_two_factor, :model => true)
@@ -0,0 +1,46 @@
1
+ require 'rails/generators'
2
+
3
+ module DeviseTwilioTwoFactor
4
+ module Generators
5
+ class DeviseTwilioTwoFactorGenerator < Rails::Generators::NamedBase
6
+ desc 'Creates a migration to add the required attributes to NAME, and ' \
7
+ 'adds the necessary Devise directives to the model'
8
+
9
+ def install_devise_twilio_two_factor
10
+ create_devise_twilio_two_factor_migration
11
+ inject_devise_directives_into_model
12
+ end
13
+
14
+ private
15
+
16
+ def create_devise_twilio_two_factor_migration
17
+ migration_arguments = [
18
+ "add_devise_twilio_two_factor_to_#{plural_name}",
19
+ "otp_required_for_login:boolean",
20
+ ]
21
+
22
+ Rails::Generators.invoke('active_record:migration', migration_arguments)
23
+ end
24
+
25
+ def inject_devise_directives_into_model
26
+ model_path = File.join('app', 'models', "#{file_path}.rb")
27
+
28
+ class_path = if namespaced?
29
+ class_name.to_s.split("::")
30
+ else
31
+ [class_name]
32
+ end
33
+
34
+ indent_depth = class_path.size
35
+
36
+ content = [
37
+ "devise :twilio_two_factor_authenticatable"
38
+ ]
39
+
40
+ content = content.map { |line| " " * indent_depth + line }.join("\n") << "\n"
41
+
42
+ inject_into_class(model_path, class_path.last, content)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ module DeviseTwilioTwoFactor
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ class TwilioTwoFactorAuthenticatableDouble
4
+ extend ::ActiveModel::Callbacks
5
+ include ::ActiveModel::Validations::Callbacks
6
+ extend ::Devise::Models
7
+
8
+ devise :twilio_two_factor_authenticatable, otp_destination: "phone", communication_type: "sms"
9
+ end
10
+
11
+ RSpec.describe ::Devise::Models::TwilioTwoFactorAuthenticatable do
12
+ context 'When included in a class' do
13
+ subject { TwilioTwoFactorAuthenticatableDouble.new }
14
+
15
+ it 'creates class variables with the options passed' do
16
+ expect(subject.class.otp_destination).to eq("phone")
17
+ expect(subject.class.communication_type).to eq("sms")
18
+ end
19
+
20
+ it 'has access to class variables defined in Devise' do
21
+ expect(subject.class.otp_code_length).to eq(6)
22
+ expect(subject.class.twilio_account_sid).to eq("ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
23
+ expect(subject.class.twilio_auth_token).to eq("token")
24
+ expect(subject.class.twilio_verify_service_sid).to eq("VAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
25
+ expect(subject.class.second_factor_resource_id).to eq("id")
26
+ expect(subject.class.remember_otp_session_for_seconds).to eq(30.minutes)
27
+ end
28
+
29
+ describe ".send_otp_code" do
30
+ let(:twilio_client) { instance_double(TwilioTwoFactorAuthClient) }
31
+ before do
32
+ allow_any_instance_of(TwilioTwoFactorAuthClient).to receive(:send_code) { true }
33
+ allow(TwilioTwoFactorAuthClient).to receive(:new).and_return(twilio_client)
34
+ end
35
+
36
+ it 'instanciates twilio client and tells client to send code' do
37
+ expect(TwilioTwoFactorAuthClient).to receive(:new).with(subject)
38
+ expect(twilio_client).to receive(:send_code)
39
+
40
+ subject.send_otp_code
41
+ end
42
+ end
43
+
44
+ describe '.verify_otp_code' do
45
+ let(:twilio_client) { instance_double(TwilioTwoFactorAuthClient) }
46
+ let(:code) { "123456" }
47
+ before do
48
+ allow_any_instance_of(TwilioTwoFactorAuthClient).to receive(:verify_code).with(code) { true }
49
+ allow(TwilioTwoFactorAuthClient).to receive(:new).and_return(twilio_client)
50
+ end
51
+
52
+ it 'instantiates twilio client and calls verify_code' do
53
+ expect(TwilioTwoFactorAuthClient).to receive(:new).with(subject)
54
+ expect(twilio_client).to receive(:verify_code).with(code)
55
+
56
+ subject.verify_otp_code(code)
57
+ end
58
+ end
59
+
60
+ describe '.login_attempts_exceeded?' do
61
+ it 'should return false unless locked_at is defined' do
62
+ allow_any_instance_of(TwilioTwoFactorAuthenticatableDouble).to receive(:failed_attempts) { 0 }
63
+ expect(subject.login_attempts_exceeded?).to eq(false)
64
+ end
65
+
66
+ it 'should return true if login_attempts exceeds max login attempts' do
67
+ allow_any_instance_of(TwilioTwoFactorAuthenticatableDouble).to receive(:failed_attempts) { 100 }
68
+
69
+ expect(subject.login_attempts_exceeded?).to eq(true)
70
+ end
71
+ end
72
+
73
+ describe '.send_new_otp_after_login?' do
74
+ it 'should return false if otp_required_for_login is false' do
75
+ allow_any_instance_of(TwilioTwoFactorAuthenticatableDouble).to receive(:otp_required_for_login) { false }
76
+
77
+ expect(subject.send_new_otp_after_login?).to eq(false)
78
+ end
79
+
80
+ it 'should return true if otp_required_for_login is true' do
81
+ allow_any_instance_of(TwilioTwoFactorAuthenticatableDouble).to receive(:otp_required_for_login) { true }
82
+
83
+ expect(subject.send_new_otp_after_login?).to eq(true)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ class TwilioTwoFactorAuthenticatableDouble
4
+ extend ::ActiveModel::Callbacks
5
+ include ::ActiveModel::Validations::Callbacks
6
+ extend ::Devise::Models
7
+
8
+ devise :twilio_two_factor_authenticatable, otp_destination: "phone", communication_type: "sms"
9
+ end
10
+
11
+ RSpec.describe TwilioTwoFactorAuthClient, type: :service do
12
+ let(:twilio_client) { instance_double(Twilio::REST::Client) }
13
+ let(:mock_number) { "+16309437616" }
14
+ let(:mock_twilio_response) { instance_double("twilio_response") }
15
+ let(:mock_code) { "123456" }
16
+
17
+ before do
18
+ allow_any_instance_of(TwilioTwoFactorAuthenticatableDouble).to receive(:phone).and_return(mock_number)
19
+ allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
20
+ end
21
+
22
+ describe 'new' do
23
+ it 'should instantiate twilio client' do
24
+ expect(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
25
+
26
+ TwilioTwoFactorAuthClient.new(TwilioTwoFactorAuthenticatableDouble.new)
27
+ end
28
+ end
29
+
30
+ describe '#send_code' do
31
+ describe 'code was send successfully' do
32
+ it 'should have status pending and return true' do
33
+ allow(mock_twilio_response).to receive(:status).and_return("pending")
34
+ allow(twilio_client).to receive_message_chain(:verify, :v2, :services, :verifications, :create).and_return(mock_twilio_response)
35
+ expect(twilio_client).to receive_message_chain(:verify, :v2, :services, :verifications, :create)
36
+
37
+ response = TwilioTwoFactorAuthClient.new(TwilioTwoFactorAuthenticatableDouble.new).send_code
38
+ expect(response).to eq(true)
39
+ end
40
+ end
41
+
42
+ describe 'failed to send code' do
43
+ it 'it should have status cancelled and return false' do
44
+ allow(mock_twilio_response).to receive(:status).and_return("cancelled")
45
+ allow(twilio_client).to receive_message_chain(:verify, :v2, :services, :verifications, :create).and_return(mock_twilio_response)
46
+ expect(twilio_client).to receive_message_chain(:verify, :v2, :services, :verifications, :create)
47
+
48
+ response = TwilioTwoFactorAuthClient.new(TwilioTwoFactorAuthenticatableDouble.new).send_code
49
+ expect(response).to eq(false)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '#verify_code' do
55
+ describe 'code was verified' do
56
+ it 'should return a status of approved and true' do
57
+ allow(mock_twilio_response).to receive(:status).and_return("approved")
58
+ allow(twilio_client).to receive_message_chain(:verify, :v2, :services, :verification_checks, :create).and_return(mock_twilio_response)
59
+ expect(twilio_client).to receive_message_chain(:verify, :v2, :services, :verification_checks, :create)
60
+
61
+ response = TwilioTwoFactorAuthClient.new(TwilioTwoFactorAuthenticatableDouble.new).verify_code(mock_code)
62
+ expect(response).to eq(true)
63
+ end
64
+ end
65
+
66
+ describe 'code could not be verified' do
67
+ it 'should return a status of pending and false' do
68
+ allow(mock_twilio_response).to receive(:status).and_return("pending")
69
+ allow(twilio_client).to receive_message_chain(:verify, :v2, :services, :verification_checks, :create).and_return(mock_twilio_response)
70
+ expect(twilio_client).to receive_message_chain(:verify, :v2, :services, :verification_checks, :create)
71
+
72
+ response = TwilioTwoFactorAuthClient.new(TwilioTwoFactorAuthenticatableDouble.new).verify_code(mock_code)
73
+ expect(response).to eq(false)
74
+ end
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "devise_twilio_two_factor"
4
+ require 'active_model'
5
+ require 'devise'
6
+
7
+ RSpec.configure do |config|
8
+ # Enable flags like --only-failures and --next-failure
9
+ config.example_status_persistence_file_path = ".rspec_status"
10
+
11
+ # Disable RSpec exposing methods globally on `Module` and `main`
12
+ config.disable_monkey_patching!
13
+
14
+ config.expect_with :rspec do |c|
15
+ c.syntax = :expect
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devise_twilio_two_factor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - John Livingston
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: devise
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 4.8.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 4.8.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: twilio-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.66.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.66.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: '3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activemodel
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Devise Twilio Verify Two Factor Authentication
112
+ email: jclivingston316@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - ".github/workflows/main.yml"
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - app/controllers/devise/twilio_two_factor_controller.rb
126
+ - app/views/devise/twilio_two_factor/show.html.erb
127
+ - bin/console
128
+ - bin/setup
129
+ - config/locales/de.yml
130
+ - config/locales/en.yml
131
+ - config/locales/es.yml
132
+ - config/locales/fr.yml
133
+ - config/locales/ru.yml
134
+ - devise_twilio_two_factor.gemspec
135
+ - lib/devise_twilio_two_factor.rb
136
+ - lib/devise_twilio_two_factor/callbacks.rb
137
+ - lib/devise_twilio_two_factor/callbacks/twilio_two_factor_authenticatable.rb
138
+ - lib/devise_twilio_two_factor/controllers.rb
139
+ - lib/devise_twilio_two_factor/controllers/helpers.rb
140
+ - lib/devise_twilio_two_factor/models.rb
141
+ - lib/devise_twilio_two_factor/models/twilio_two_factor_authenticatable.rb
142
+ - lib/devise_twilio_two_factor/rails.rb
143
+ - lib/devise_twilio_two_factor/routes.rb
144
+ - lib/devise_twilio_two_factor/services.rb
145
+ - lib/devise_twilio_two_factor/services/twilio_two_factor_client.rb
146
+ - lib/devise_twilio_two_factor/version.rb
147
+ - lib/generators/devise_twilio_two_factor/devise_twilio_two_factor_generator.rb
148
+ - sig/devise_twilio_two_factor.rbs
149
+ - spec/models/twilio_two_factor_authenticatable_spec.rb
150
+ - spec/services/twilio_two_factor_auth_client_spec.rb
151
+ - spec/spec_helper.rb
152
+ homepage: https://github.com/jliv316/devise-twilio-two-factor
153
+ licenses:
154
+ - MIT
155
+ metadata:
156
+ homepage_uri: https://github.com/jliv316/devise-twilio-two-factor
157
+ source_code_uri: https://github.com/Jliv316/devise_twilio_two_factor
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: 2.6.0
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubygems_version: 3.3.7
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: Devise Twilio Verify Two Factor Authentication
177
+ test_files:
178
+ - spec/models/twilio_two_factor_authenticatable_spec.rb
179
+ - spec/services/twilio_two_factor_auth_client_spec.rb
180
+ - spec/spec_helper.rb