devise-twilio-verify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.github/workflows/build.yml +32 -0
- data/.gitignore +45 -0
- data/.rspec +2 -0
- data/Appraisals +22 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.md +266 -0
- data/Rakefile +29 -0
- data/app/assets/javascripts/devise_twilio_verify.js +12 -0
- data/app/assets/stylesheets/devise_twilio_verify.css +26 -0
- data/app/assets/stylesheets/devise_twilio_verify.sass +24 -0
- data/app/controllers/devise/devise_twilio_verify_controller.rb +183 -0
- data/app/controllers/devise_twilio_verify/passwords_controller.rb +30 -0
- data/app/services/twilio_verify_service.rb +66 -0
- data/app/views/devise/enable_twilio_verify.html.erb +7 -0
- data/app/views/devise/enable_twilio_verify.html.haml +5 -0
- data/app/views/devise/verify_twilio_verify.html.erb +16 -0
- data/app/views/devise/verify_twilio_verify.html.haml +13 -0
- data/app/views/devise/verify_twilio_verify_installation.html.erb +18 -0
- data/app/views/devise/verify_twilio_verify_installation.html.haml +16 -0
- data/config/locales/en.yml +27 -0
- data/config.ru +9 -0
- data/devise-twilio-verify.gemspec +49 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5_2.gemfile +14 -0
- data/gemfiles/rails_6.gemfile +15 -0
- data/lib/devise-twilio-verify/controllers/helpers.rb +87 -0
- data/lib/devise-twilio-verify/controllers/view_helpers.rb +50 -0
- data/lib/devise-twilio-verify/hooks/twilio_verify_authenticatable.rb +7 -0
- data/lib/devise-twilio-verify/mapping.rb +16 -0
- data/lib/devise-twilio-verify/models/twilio_verify_authenticatable.rb +21 -0
- data/lib/devise-twilio-verify/models/twilio_verify_lockable.rb +43 -0
- data/lib/devise-twilio-verify/rails.rb +16 -0
- data/lib/devise-twilio-verify/routes.rb +21 -0
- data/lib/devise-twilio-verify/version.rb +5 -0
- data/lib/devise-twilio-verify.rb +32 -0
- data/lib/generators/active_record/devise_twilio_verify_generator.rb +23 -0
- data/lib/generators/active_record/templates/migration.rb +18 -0
- data/lib/generators/devise_twilio_verify/devise_twilio_verify_generator.rb +30 -0
- data/lib/generators/devise_twilio_verify/install_generator.rb +80 -0
- metadata +343 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
class Devise::DeviseTwilioVerifyController < DeviseController
|
2
|
+
prepend_before_action :find_resource, :only => [
|
3
|
+
:request_sms
|
4
|
+
]
|
5
|
+
prepend_before_action :find_resource_and_require_password_checked, :only => [
|
6
|
+
:GET_verify_twilio_verify, :POST_verify_twilio_verify
|
7
|
+
]
|
8
|
+
|
9
|
+
prepend_before_action :check_resource_not_twilio_verify_enabled, :only => [
|
10
|
+
:GET_verify_twilio_verify_installation, :POST_verify_twilio_verify_installation
|
11
|
+
]
|
12
|
+
|
13
|
+
prepend_before_action :authenticate_scope!, :only => [
|
14
|
+
:GET_enable_twilio_verify, :POST_enable_twilio_verify, :GET_verify_twilio_verify_installation,
|
15
|
+
:POST_verify_twilio_verify_installation, :POST_disable_twilio_verify
|
16
|
+
]
|
17
|
+
|
18
|
+
include Devise::Controllers::Helpers
|
19
|
+
|
20
|
+
def GET_verify_twilio_verify
|
21
|
+
render :verify_twilio_verify
|
22
|
+
end
|
23
|
+
|
24
|
+
# verify 2fa
|
25
|
+
def POST_verify_twilio_verify
|
26
|
+
if @resource.mobile_phone.blank? || params[:token].blank?
|
27
|
+
return handle_invalid_token :verify_twilio_verify, :invalid_token
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
verification_check = TwilioVerifyService.verify_sms_token(@resource.mobile_phone, params[:token])
|
32
|
+
verification_check = verification_check.status == 'approved'
|
33
|
+
rescue Twilio::REST::RestError
|
34
|
+
verification_check = false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Hack to reproduce authy functionality of being able to verify 2FA via SMS or TOTP
|
38
|
+
# not ideal as there could be network delays, but there is currently no alternative
|
39
|
+
if !verification_check && @resource.twilio_totp_factor_sid.present?
|
40
|
+
verification_check = TwilioVerifyService.verify_totp_token(@resource, params[:token])
|
41
|
+
verification_check = verification_check.status == 'approved'
|
42
|
+
end
|
43
|
+
|
44
|
+
if verification_check
|
45
|
+
remember_device(@resource.id) if params[:remember_device].to_i == 1
|
46
|
+
remember_user
|
47
|
+
record_twilio_verify_authentication
|
48
|
+
respond_with resource, :location => after_sign_in_path_for(@resource)
|
49
|
+
else
|
50
|
+
handle_invalid_token :verify_twilio_verify, :invalid_token
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# enable 2fa
|
55
|
+
def POST_enable_twilio_verify
|
56
|
+
if resource.update(twilio_verify_enabled: true)
|
57
|
+
redirect_to [resource_name, :verify_twilio_verify_installation] and return
|
58
|
+
else
|
59
|
+
set_flash_message(:error, :not_enabled)
|
60
|
+
redirect_to after_twilio_verify_enabled_path_for(resource) and return
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Disable 2FA
|
65
|
+
def POST_disable_twilio_verify
|
66
|
+
resource.assign_attributes(twilio_verify_enabled: false)
|
67
|
+
resource.save(:validate => false)
|
68
|
+
redirect_to after_twilio_verify_disabled_path_for(resource)
|
69
|
+
end
|
70
|
+
|
71
|
+
def GET_verify_twilio_verify_installation
|
72
|
+
if resource_class.twilio_verify_enable_qr_code
|
73
|
+
#response = Authy::API.request_qr_code(id: resource.authy_id)
|
74
|
+
#@twilio_verify_qr_code = response.qr_code
|
75
|
+
end
|
76
|
+
render :verify_twilio_verify_installation
|
77
|
+
end
|
78
|
+
|
79
|
+
def POST_verify_twilio_verify_installation
|
80
|
+
if @resource.mobile_phone.blank? || params[:token].blank?
|
81
|
+
return handle_invalid_token :verify_twilio_verify_installation, :not_enabled
|
82
|
+
end
|
83
|
+
|
84
|
+
verification_check = TwilioVerifyService.verify_sms_token(@resource.mobile_phone, params[:token])
|
85
|
+
|
86
|
+
self.resource.twilio_verify_enabled = token.ok?
|
87
|
+
|
88
|
+
if token.ok? && self.resource.save
|
89
|
+
remember_device(@resource.id) if params[:remember_device].to_i == 1
|
90
|
+
record_twilio_verify_authentication
|
91
|
+
set_flash_message(:notice, :enabled)
|
92
|
+
redirect_to after_twilio_verify_verified_path_for(resource)
|
93
|
+
else
|
94
|
+
if resource_class.twilio_verify_enable_qr_code
|
95
|
+
#response = Authy::API.request_qr_code(id: resource.authy_id)
|
96
|
+
#@twilio_verify_qr_code = response.qr_code
|
97
|
+
end
|
98
|
+
handle_invalid_token :verify_twilio_verify_installation, :not_enabled
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def request_sms
|
103
|
+
if @resource.blank? || @resource.mobile_phone.blank?
|
104
|
+
render :json => {:sent => false, :message => "User couldn't be found."}
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
verification = TwilioVerifyService.send_sms_token(@resource.mobile_phone)
|
109
|
+
success = verification.status == 'pending'
|
110
|
+
|
111
|
+
render json: {
|
112
|
+
sent: success,
|
113
|
+
message: success ? 'Token was sent.' : 'Token was not sent, please try again.'
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def authenticate_scope!
|
120
|
+
send(:"authenticate_#{resource_name}!", :force => true)
|
121
|
+
self.resource = send("current_#{resource_name}")
|
122
|
+
@resource = resource
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_resource
|
126
|
+
@resource = send("current_#{resource_name}")
|
127
|
+
|
128
|
+
if @resource.nil?
|
129
|
+
@resource = resource_class.find_by_id(session["#{resource_name}_id"])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def find_resource_and_require_password_checked
|
134
|
+
find_resource
|
135
|
+
|
136
|
+
if @resource.nil? || session[:"#{resource_name}_password_checked"].to_s != "true"
|
137
|
+
redirect_to invalid_resource_path
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def check_resource_not_twilio_verify_enabled
|
142
|
+
if resource.twilio_verify_enabled
|
143
|
+
redirect_to after_twilio_verify_verified_path_for(resource)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def after_twilio_verify_enabled_path_for(resource)
|
150
|
+
root_path
|
151
|
+
end
|
152
|
+
|
153
|
+
def after_twilio_verify_verified_path_for(resource)
|
154
|
+
after_twilio_verify_enabled_path_for(resource)
|
155
|
+
end
|
156
|
+
|
157
|
+
def after_twilio_verify_disabled_path_for(resource)
|
158
|
+
root_path
|
159
|
+
end
|
160
|
+
|
161
|
+
def invalid_resource_path
|
162
|
+
root_path
|
163
|
+
end
|
164
|
+
|
165
|
+
def handle_invalid_token(view, error_message)
|
166
|
+
if @resource.respond_to?(:invalid_twilio_verify_attempt!) && @resource.invalid_twilio_verify_attempt!
|
167
|
+
after_account_is_locked
|
168
|
+
else
|
169
|
+
set_flash_message(:error, error_message)
|
170
|
+
render view
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def after_account_is_locked
|
175
|
+
sign_out_and_redirect @resource
|
176
|
+
end
|
177
|
+
|
178
|
+
def remember_user
|
179
|
+
if session.delete("#{resource_name}_remember_me") == true && @resource.respond_to?(:remember_me=)
|
180
|
+
@resource.remember_me = true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class DeviseTwilioVerify::PasswordsController < Devise::PasswordsController
|
2
|
+
##
|
3
|
+
# In the passwords controller a user can update their password using a
|
4
|
+
# recovery token. If `Devise.sign_in_after_reset_password` is `true` then the
|
5
|
+
# user is signed in immediately with the
|
6
|
+
# `Devise::Controllers::SignInOut#sign_in` method. However, if the user has
|
7
|
+
# 2FA enabled they should enter their second factor before they are signed in.
|
8
|
+
#
|
9
|
+
# This method overrides `Devise::Controllers::SignInOut#sign_in` but only
|
10
|
+
# within the `Devise::PasswordsController`. If the user needs to verify 2FA
|
11
|
+
# then `sign_in` returns `true`. This short circuits the method before it can
|
12
|
+
# call `warden.set_user` and log the user in.
|
13
|
+
#
|
14
|
+
# The user is redirected to `after_resetting_password_path_for(user)` at which
|
15
|
+
# point, since the user is not logged in, redirects again to sign in.
|
16
|
+
#
|
17
|
+
# This doesn't retain the expected behaviour of
|
18
|
+
# `Devise.sign_in_after_reset_password`, but is forgivable because this
|
19
|
+
# shouldn't be an avenue to bypass 2FA.
|
20
|
+
def sign_in(resource_or_scope, *args)
|
21
|
+
resource = args.last || resource_or_scope
|
22
|
+
|
23
|
+
if resource.respond_to?(:with_twilio_verify_authentication?) && resource.with_twilio_verify_authentication?(request)
|
24
|
+
# Do nothing. Because we need verify the 2FA
|
25
|
+
true
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class TwilioVerifyService
|
2
|
+
attr_reader :twilio_client, :twilio_account_sid, :twilio_auth_token, :twilio_verify_service_sid
|
3
|
+
|
4
|
+
def self.send_sms_token(phone_number)
|
5
|
+
new.twilio_verify_service.verifications.create(to: e164_format(phone_number), channel: 'sms')
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.verify_sms_token(phone_number, token)
|
9
|
+
new.twilio_verify_service.verification_checks.create(to: e164_format(phone_number), code: token)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.verify_totp_token(user, token)
|
13
|
+
new.twilio_verify_service_v2
|
14
|
+
.entities([Rails.env, user.id].join('-'))
|
15
|
+
.challenges
|
16
|
+
.create(auth_payload: token, factor_sid: user.twilio_totp_factor_sid)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.setup_totp_service(user)
|
20
|
+
new_factor = new.twilio_verify_service_v2
|
21
|
+
.entities([Rails.env, user.id].join('-'))
|
22
|
+
.new_factors
|
23
|
+
.create(friendly_name: user.to_s, factor_type: 'totp')
|
24
|
+
|
25
|
+
user.update(twilio_totp_factor_sid: new_factor.sid)
|
26
|
+
|
27
|
+
# Now in your app, take the new_factor.binding.uri
|
28
|
+
# and generate a qr code to present to the user to scan to add the app to their authenticator app
|
29
|
+
new_factor
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.register_totp_service(user, token)
|
33
|
+
# After user adds the app to their authenticator app, register the user by having them confirm a token
|
34
|
+
# if this returns factor.status == 'verified', the user has been properly setup
|
35
|
+
new.twilio_verify_service_v2
|
36
|
+
.entities([Rails.env, user.id].join('-'))
|
37
|
+
.factors(user.twilio_totp_factor_sid)
|
38
|
+
.update(auth_payload: token)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.e164_format(phone_number)
|
42
|
+
"+1#{phone_number.gsub(/[^0-9a-z\\s]/i, '')}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@twilio_account_sid = Rails.application.credentials.twilio_account_sid || ENV['TWILIO_ACCOUNT_SID']
|
47
|
+
@twilio_auth_token = Rails.application.credentials.twilio_auth_token || ENV['TWILIO_AUTH_TOKEN']
|
48
|
+
@twilio_verify_service_sid = Rails.application.credentials.twilio_verify_service_sid || ENV['TWILIO_VERIFY_SERVICE_SID']
|
49
|
+
|
50
|
+
raise 'Missing Twilio credentials' unless @twilio_account_sid && @twilio_auth_token && @twilio_verify_service_sid
|
51
|
+
|
52
|
+
@twilio_client = Twilio::REST::Client.new(@twilio_account_sid, @twilio_auth_token)
|
53
|
+
end
|
54
|
+
|
55
|
+
def twilio_verify_service
|
56
|
+
twilio_client.verify.services(twilio_verify_service_sid)
|
57
|
+
end
|
58
|
+
|
59
|
+
def twilio_verify_service_v2
|
60
|
+
twilio_client.verify.v2.services(twilio_verify_service_sid)
|
61
|
+
end
|
62
|
+
|
63
|
+
def e164_format(phone_number)
|
64
|
+
self.class.e164_format(phone_number)
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<h2><%= I18n.t('twilio_verify_register_title', scope: 'devise') %></h2>
|
2
|
+
|
3
|
+
<%= enable_twilio_verify_form do %>
|
4
|
+
<%= text_field_tag :country_code, '', :autocomplete => :off, :placeholder => I18n.t('devise.country'), :id => "twilio-verify-countries"%>
|
5
|
+
<%= text_field_tag :cellphone, '', :autocomplete => :off, :placeholder => I18n.t('devise.cellphone'), :id => "twilio-verify-cellphone"%>
|
6
|
+
<p><%= submit_tag I18n.t('enable_twilio_verify', scope: 'devise') %></p>
|
7
|
+
<% end %>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
%h2= I18n.t('twilio_verify_register_title', scope: 'devise')
|
2
|
+
= enable_twilio_verify_form do
|
3
|
+
= text_field_tag :country_code, '', :autocomplete => :off, :placeholder => I18n.t('devise.country'), :id => "twilio-verify-countries"
|
4
|
+
= text_field_tag :cellphone, '', :autocomplete => :off, :placeholder => I18n.t('devise.cellphone'), :id => "twilio-verify-cellphone"
|
5
|
+
%p= submit_tag I18n.t('enable_twilio_verify', scope: 'devise')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<h2>
|
2
|
+
<%= I18n.t('submit_token_title', scope: 'devise') %>
|
3
|
+
</h2>
|
4
|
+
|
5
|
+
<%= verify_twilio_verify_form do %>
|
6
|
+
<legend><%= I18n.t('submit_token_title', scope: 'devise') %></legend>
|
7
|
+
<%= label_tag 'twilio-verify-token' %>
|
8
|
+
<%= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'twilio-verify-token' %>
|
9
|
+
<label>
|
10
|
+
<%= check_box_tag :remember_device %>
|
11
|
+
<span><%= I18n.t('remember_device', scope: 'devise') %></span>
|
12
|
+
</label>
|
13
|
+
|
14
|
+
<%= twilio_verify_request_sms_link %>
|
15
|
+
<%= submit_tag I18n.t('submit_token', scope: 'devise'), :class => 'btn' %>
|
16
|
+
<% end %>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
%h2= I18n.t('twilio_verify_register_title', scope: 'devise')
|
2
|
+
|
3
|
+
= verify_twilio_verify_form do
|
4
|
+
%legend= I18n.t('submit_token_title', scope: 'devise')
|
5
|
+
= hidden_field_tag :"#{resource_name}_id", @resource.id
|
6
|
+
= label_tag 'twilio-verify-token'
|
7
|
+
= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'twilio-verify-token'
|
8
|
+
%label
|
9
|
+
= check_box_tag :remember_device
|
10
|
+
%span= I18n.t('remember_device', scope: 'devise')
|
11
|
+
|
12
|
+
= twilio_verify_request_sms_link
|
13
|
+
= submit_tag I18n.t('submit_token', scope: 'devise'), :class => 'btn'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<h2><%= I18n.t('twilio_verify_verify_installation_title', scope: 'devise') %></h2>
|
2
|
+
|
3
|
+
<% if @twilio_verify_qr_code %>
|
4
|
+
<%= image_tag @twilio_verify_qr_code, :size => '256x256', :alt => I18n.t('twilio_verify_qr_code_alt', scope: 'devise') %>
|
5
|
+
<p><%= I18n.t('twilio_verify_qr_code_instructions', scope: 'devise') %></p>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<%= verify_twilio_verify_installation_form do %>
|
9
|
+
<legend><%= I18n.t('submit_token_title', scope: 'devise') %></legend>
|
10
|
+
<%= label_tag :token %>
|
11
|
+
<%= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'twilio-verify-token' %>
|
12
|
+
<label>
|
13
|
+
<%= check_box_tag :remember_device %>
|
14
|
+
<span><%= I18n.t('remember_device', scope: 'devise') %></span>
|
15
|
+
</label>
|
16
|
+
<%= twilio_verify_request_sms_link %>
|
17
|
+
<%= submit_tag I18n.t('enable_my_account', scope: 'devise'), :class => 'btn' %>
|
18
|
+
<% end %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
%h2= I18n.t('twilio_verify_verify_installation_title', scope: 'devise')
|
2
|
+
|
3
|
+
- if @twilio_verify_qr_code
|
4
|
+
= image_tag @twilio_verify_qr_code, :size => '256x256', :alt => I18n.t('twilio_verify_qr_code_alt', scope: 'devise')
|
5
|
+
%p= I18n.t('twilio_verify_qr_code_instructions', scope: 'devise')
|
6
|
+
|
7
|
+
= verify_twilio_verify_installation_form do
|
8
|
+
%legend= I18n.t('submit_token_title', scope: 'devise')
|
9
|
+
= label_tag :token
|
10
|
+
= text_field_tag :token, "", :autocomplete => "one-time-code", :inputmode => "numeric", :pattern => "[0-9]*", :id => 'twilio-verify-token'
|
11
|
+
%label
|
12
|
+
= check_box_tag :remember_device
|
13
|
+
%span= I18n.t('remember_device', scope: 'devise')
|
14
|
+
= twilio_verify_request_sms_link
|
15
|
+
= submit_tag I18n.t('enable_my_account', scope: 'devise'), :class => 'btn'
|
16
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
en:
|
2
|
+
devise:
|
3
|
+
submit_token: 'Check Token'
|
4
|
+
submit_token_title: 'Please enter your auth token:'
|
5
|
+
twilio_verify_register_title: 'Enable Two factor authentication'
|
6
|
+
enable_twilio_verify: 'Enable'
|
7
|
+
cellphone: 'Enter your cellphone'
|
8
|
+
country: 'Enter your country'
|
9
|
+
request_sms: 'Request SMS'
|
10
|
+
remember_device: 'Remember Device'
|
11
|
+
request_to_login: 'Request to Login'
|
12
|
+
|
13
|
+
twilio_verify_verify_installation_title: 'Verify your account'
|
14
|
+
enable_my_account: 'Enable my account'
|
15
|
+
|
16
|
+
twilio_verify_qr_code_alt: 'QR code for scanning with your authenticator app.'
|
17
|
+
twilio_verify_qr_code_instructions: 'Scan this QR code with your authenticator application and enter the code below.'
|
18
|
+
|
19
|
+
devise_twilio_verify:
|
20
|
+
user:
|
21
|
+
enabled: 'Two factor authentication was enabled'
|
22
|
+
not_enabled: 'Something went wrong while enabling two factor authentication'
|
23
|
+
disabled: 'Two factor authentication was disabled'
|
24
|
+
not_disabled: 'Something went wrong while disabling two factor authentication'
|
25
|
+
signed_in: 'Signed in with two factor authentication successfully.'
|
26
|
+
already_enabled: 'Two factor authentication is already enabled.'
|
27
|
+
invalid_token: 'The entered token is invalid'
|
data/config.ru
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "devise-twilio-verify/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "devise-twilio-verify"
|
9
|
+
spec.version = DeviseTwilioVerify::VERSION
|
10
|
+
spec.authors = ["Jay Wolff"]
|
11
|
+
|
12
|
+
spec.summary = %q{Twilio Verify plugin to add two factor authentication to Devise.}
|
13
|
+
spec.description = %q{Twilio Verify plugin to add two factor authentication to Devise. This gem is meant to make migrating from authy to twilio verify as simple as possible, please see the README for details.}
|
14
|
+
spec.homepage = "https://github.com/jayywolff/twilio-verify-devise"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.metadata = {
|
18
|
+
"bug_tracker_uri" => "https://github.com/jayywolff/twilio-verify-devise/issues",
|
19
|
+
"change_log_uri" => "https://github.com/jayywolff/twilio-verify-devise/blob/master/CHANGELOG.md",
|
20
|
+
"documentation_uri" => "https://github.com/jayywolff/twilio-verify-devise",
|
21
|
+
"homepage_uri" => "https://github.com/jayywolff/twilio-verify-devise",
|
22
|
+
"source_code_uri" => "https://github.com/jayywolff/twilio-verify-devise"
|
23
|
+
}
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
f.match(%r{^(test|spec|features)/})
|
27
|
+
end
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "devise", ">= 4.0.0"
|
31
|
+
spec.add_dependency "twilio-ruby", "~> 5.74"
|
32
|
+
|
33
|
+
spec.add_development_dependency "appraisal", "~> 2.2"
|
34
|
+
spec.add_development_dependency "bundler", ">= 1.16"
|
35
|
+
spec.add_development_dependency "rake"
|
36
|
+
spec.add_development_dependency "combustion", "~> 1.1"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "rspec-rails"
|
39
|
+
spec.add_development_dependency "rails-controller-testing", "~> 1.0"
|
40
|
+
spec.add_development_dependency "yard", "~> 0.9.11"
|
41
|
+
spec.add_development_dependency "rdoc", "~> 4.3.0"
|
42
|
+
spec.add_development_dependency "simplecov", "~> 0.17.1"
|
43
|
+
spec.add_development_dependency "webmock", "~> 3.11.0"
|
44
|
+
spec.add_development_dependency "rails", ">= 5"
|
45
|
+
spec.add_development_dependency "sqlite3"
|
46
|
+
spec.add_development_dependency "generator_spec"
|
47
|
+
spec.add_development_dependency "database_cleaner", "~> 1.7"
|
48
|
+
spec.add_development_dependency "factory_bot_rails", "~> 5.1.1"
|
49
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "rails", "~> 5.2.0"
|
6
|
+
gem "sqlite3", "~> 1.3.13"
|
7
|
+
|
8
|
+
group :development, :test do
|
9
|
+
gem "factory_girl_rails", require: false
|
10
|
+
gem "rspec-rails", "~>4.0.0.beta3", require: false
|
11
|
+
gem "database_cleaner", require: false
|
12
|
+
end
|
13
|
+
|
14
|
+
gemspec path: "../"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "rails", "~> 6.0.0"
|
6
|
+
gem "sqlite3", "~> 1.4"
|
7
|
+
gem "net-smtp"
|
8
|
+
|
9
|
+
group :development, :test do
|
10
|
+
gem "factory_girl_rails", require: false
|
11
|
+
gem "rspec-rails", "~>4.0.0.beta3", require: false
|
12
|
+
gem "database_cleaner", require: false
|
13
|
+
end
|
14
|
+
|
15
|
+
gemspec path: "../"
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module DeviseTwilioVerify
|
2
|
+
module Controllers
|
3
|
+
module Helpers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :check_request_and_redirect_to_verify_token, :if => :is_signing_in?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def remember_device(id)
|
13
|
+
cookies.signed[:remember_device] = {
|
14
|
+
:value => {expires: Time.now.to_i, id: id}.to_json,
|
15
|
+
:secure => !(Rails.env.test? || Rails.env.development?),
|
16
|
+
:httponly => !(Rails.env.test? || Rails.env.development?),
|
17
|
+
:expires => resource_class.twilio_verify_remember_device.from_now
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def forget_device
|
22
|
+
cookies.delete :remember_device
|
23
|
+
end
|
24
|
+
|
25
|
+
def require_token?
|
26
|
+
id = warden.session(resource_name)[:id]
|
27
|
+
cookie = cookies.signed[:remember_device]
|
28
|
+
return true if cookie.blank?
|
29
|
+
|
30
|
+
# require token for old cookies which just have expiration time and no id
|
31
|
+
return true if cookie.to_s =~ %r{\A\d+\z}
|
32
|
+
|
33
|
+
cookie = JSON.parse(cookie) rescue ""
|
34
|
+
return cookie.blank? || (Time.now.to_i - cookie['expires'].to_i) > \
|
35
|
+
resource_class.twilio_verify_remember_device.to_i || cookie['id'] != id
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_devise_sessions_controller?
|
39
|
+
self.class == Devise::SessionsController || self.class.ancestors.include?(Devise::SessionsController)
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_signing_in?
|
43
|
+
if devise_controller? &&
|
44
|
+
is_devise_sessions_controller? &&
|
45
|
+
self.action_name == "create"
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_request_and_redirect_to_verify_token
|
53
|
+
if signed_in?(resource_name) &&
|
54
|
+
warden.session(resource_name)[:with_twilio_verify_authentication] &&
|
55
|
+
require_token?
|
56
|
+
# login with 2fa
|
57
|
+
id = warden.session(resource_name)[:id]
|
58
|
+
|
59
|
+
remember_me = (params.fetch(resource_name, {})[:remember_me].to_s == "1")
|
60
|
+
return_to = session["#{resource_name}_return_to"]
|
61
|
+
sign_out
|
62
|
+
|
63
|
+
session["#{resource_name}_id"] = id
|
64
|
+
# this is safe to put in the session because the cookie is signed
|
65
|
+
session["#{resource_name}_password_checked"] = true
|
66
|
+
session["#{resource_name}_remember_me"] = remember_me
|
67
|
+
session["#{resource_name}_return_to"] = return_to if return_to
|
68
|
+
|
69
|
+
redirect_to verify_twilio_verify_path_for(resource_name)
|
70
|
+
return
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def verify_twilio_verify_path_for(resource_or_scope = nil)
|
75
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
76
|
+
send(:"#{scope}_verify_twilio_verify_path")
|
77
|
+
end
|
78
|
+
|
79
|
+
def record_twilio_verify_authentication
|
80
|
+
@resource.update_attribute(:last_sign_in_with_twilio_verify, DateTime.now)
|
81
|
+
session["#{resource_name}_twilio_verify_token_checked"] = true
|
82
|
+
sign_in(resource_name, @resource)
|
83
|
+
set_flash_message(:notice, :signed_in) if is_navigational_format?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module DeviseTwilioVerify
|
2
|
+
module Views
|
3
|
+
module Helpers
|
4
|
+
def twilio_verify_request_sms_link(opts = {})
|
5
|
+
title = opts.delete(:title) do
|
6
|
+
I18n.t('request_sms', scope: 'devise')
|
7
|
+
end
|
8
|
+
opts = {
|
9
|
+
:id => "twilio-verify-request-sms-link",
|
10
|
+
:method => :post,
|
11
|
+
:remote => true
|
12
|
+
}.merge(opts)
|
13
|
+
|
14
|
+
link_to(
|
15
|
+
title,
|
16
|
+
url_for([resource_name.to_sym, :request_sms]),
|
17
|
+
opts
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify_twilio_verify_form(opts = {}, &block)
|
22
|
+
opts = default_opts.merge(:id => 'devise_twilio_verify').merge(opts)
|
23
|
+
form_tag([resource_name.to_sym, :verify_twilio_verify], opts) do
|
24
|
+
buffer = hidden_field_tag(:"#{resource_name}_id", @resource.id)
|
25
|
+
buffer << capture(&block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def enable_twilio_verify_form(opts = {}, &block)
|
30
|
+
opts = default_opts.merge(opts)
|
31
|
+
form_tag([resource_name.to_sym, :enable_twilio_verify], opts) do
|
32
|
+
capture(&block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def verify_twilio_verify_installation_form(opts = {}, &block)
|
37
|
+
opts = default_opts.merge(opts)
|
38
|
+
form_tag([resource_name.to_sym, :verify_twilio_verify_installation], opts) do
|
39
|
+
capture(&block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_opts
|
46
|
+
{ :class => 'twilio-verify-form', :method => :post }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Warden::Manager.after_authentication do |user, auth, options|
|
2
|
+
if user.respond_to?(:with_twilio_verify_authentication?)
|
3
|
+
if auth.session(options[:scope])[:with_twilio_verify_authentication] = user.with_twilio_verify_authentication?(auth.request)
|
4
|
+
auth.session(options[:scope])[:id] = user.id
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DeviseTwilioVerify
|
2
|
+
module Mapping
|
3
|
+
private
|
4
|
+
def default_controllers(options)
|
5
|
+
options[:controllers] ||= {}
|
6
|
+
options[:controllers][:passwords] ||= "devise_twilio_verify/passwords"
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_path_names(options)
|
11
|
+
options[:path_names] ||= {}
|
12
|
+
options[:path_names][:request_sms] ||= 'request-sms'
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|