rose_quartz 0.1.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9a4ea455229fd1c679e1f03e3af168e2957c7009
4
- data.tar.gz: 497bd1c54a984d513e7cef6da66e9229487e4de2
3
+ metadata.gz: 715723d37a50e4966319aead3e66cf26168a2302
4
+ data.tar.gz: cbf6814f33a0b4e797f71c13bbfe4deab4c355dc
5
5
  SHA512:
6
- metadata.gz: 8ef130e0e2cddad380778fd8126491e0e74abd27bcefd0f9fa1f47aca7d2ebe25daf23264eb13e85a65516809dae0f9fbefc198d3bbb39d700e49e4fe56911a7
7
- data.tar.gz: f214a522eb35570560431dbd8bfabb7c638aa7d0acae64f4903134a2b8d1087be49297a15aeec66be28eaef67c189673fc10fb5b526b56a20986c2b43133b5a4
6
+ metadata.gz: e4c51661f7211f4150ba8d89eb1410885d5ba01c3cd79ef72a2be5cc411f088aeb383e0f832c7692a52008d1a3698503aaa10366f1388a2a9eb998daf1128b48
7
+ data.tar.gz: 43bdea2ee92e8ba06d0d0a3132f17e932bc202ae9716b64b02c70167b6b204b6a6760aa43d911404258f7ade1a6c011dfe096531fb1a0e914db7399b9c76f39e
@@ -3,3 +3,5 @@ language: ruby
3
3
  rvm:
4
4
  - 2.3.3
5
5
  - 2.4.0
6
+ after_success:
7
+ - bundle exec codeclimate-test-reporter
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # RoseQuartz
2
2
 
3
3
  [![Build Status](https://travis-ci.org/little-bobby-tables/rose_quartz.svg?branch=master)](https://travis-ci.org/little-bobby-tables/rose_quartz)
4
+ [![Test Coverage](https://codeclimate.com/github/little-bobby-tables/rose_quartz/badges/coverage.svg)](https://codeclimate.com/github/little-bobby-tables/rose_quartz/coverage)
4
5
 
5
6
  A gem that adds two-factor authentication (time-based one-time passwords) to [Devise](https://github.com/plataformatec/devise)
6
7
  using the [rotp](https://github.com/mdp/rotp) library.
@@ -8,10 +9,105 @@ using the [rotp](https://github.com/mdp/rotp) library.
8
9
  It attempts to stay lightweight by making a lot of assumptions — for example, that
9
10
  you have a single authenticatable resource, `User`, and that you're using `ActiveRecord`.
10
11
 
11
- Highlights of *RoseQuartz* are:
12
+ #### Highlights:
12
13
 
13
- * Zero tampering with the `User` model no additional fields, no included modules.
14
- * Separate table that can be updated in future without affecting your codebase and data.
14
+ * Adds optional TOTP (compatible with Google Authenticator) to the sign-in process.
15
+ * Provides a backup code as a fallback option; resets it once it has been used and notifies the user.
16
+ * Does not tamper with the `User` model — no additional fields, no included modules.
17
+ * Employs a separate table that can be updated in future without affecting your codebase and data.
15
18
  * Built with Rails 5 and Devise 4 in mind.
16
19
 
17
- The gem is still in development. All tests for core functionality are passing, but UI integration is not implemented yet.
20
+ #### What it does not do:
21
+
22
+ Use a multiple-page login system (email and password first, two-factor authentication token next).
23
+ This introduces lots of needless complexity, which goes against the purpose of the gem.
24
+
25
+ #### What it should do, but does not (yet):
26
+
27
+ * Encrypt the backup code and the secret used to generate OTP.
28
+
29
+ ## Getting Started
30
+
31
+ First, add *RoseQuartz* to your Gemfile:
32
+
33
+ ```
34
+ gem 'rose_quartz'
35
+ ```
36
+ And run:
37
+ ```
38
+ bundle install
39
+ ```
40
+
41
+ Next, you need to copy initializers, locales, and add a migration:
42
+ ```
43
+ rails g rose_quarts:install
44
+ ```
45
+
46
+ Finally, run the migration:
47
+ ```
48
+ rails db:migrate
49
+ ```
50
+
51
+ ## Adding views
52
+
53
+ #### Signing in
54
+
55
+ You need a special field for one-time password/backup code on the sign-in page (*app/views/devise/sessions/new.html.erb*).
56
+
57
+ Here's an example:
58
+
59
+ ```
60
+ <%# E-mail and password fields %>
61
+
62
+ <div class="field">
63
+ <%= label_tag :otp, 'Two-factor authentication token' %>
64
+ <%= text_field_tag :otp, '', autocomplete: "off" %>
65
+ </div>
66
+
67
+ <%# The rest of the form %>
68
+ ```
69
+
70
+ Note that you must leave the parameter name (`otp`) intact.
71
+
72
+ #### Enabling/disabling two-factor authentication
73
+
74
+ The gem adds a special extension to Devise that allows you to
75
+ include two-factor authentication setup in the account editing page
76
+ (*app/views/devise/registrations/edit.html.erb*).
77
+
78
+ As with other settings there, a password is required to toggle two-factor authentication.
79
+ The user also needs to provide a correct token generated by their TOTP application of choice,
80
+ which ensures that their device clock is in sync with the server.
81
+
82
+ Here's a sample implementation:
83
+
84
+ ```
85
+ <div class="field">
86
+ <%= fields_for :two_factor_authentication do |tfa| %>
87
+ <% if two_factor_authentication_enabled? %>
88
+ <%= tfa.label :disable, 'Disable two-factor authentication' %>
89
+ <%= tfa.check_box :disable %>
90
+ <p>
91
+ Your backup code is <strong><%= two_factor_authentication_backup_code %></strong> -
92
+ save it to access your account if you ever lose your device or don't have it with you.
93
+ </p>
94
+ <%= tfa.label :reset_backup_code %>
95
+ <%= tfa.check_box :reset_backup_code %>
96
+ <% else %>
97
+ <%= tfa.hidden_field :secret, value: two_factor_authentication_secret %>
98
+ <%= image_tag two_factor_authentication_qr_code_uri(size: 200) %>
99
+ <p>
100
+ Scan this QR code with your device and enter the token below:
101
+ </p>
102
+ <%= tfa.label :token, 'Token' %><br />
103
+ <%= tfa.text_field :token, value: '' %>
104
+ <p>
105
+ Tip: to configure authentication on multiple devices, scan the code using each device.
106
+ </p>
107
+ <% end %>
108
+ <% end %>
109
+ </div>
110
+ ```
111
+
112
+ The following helper methods are available in the view: `two_factor_authentication_enabled?`,
113
+ `two_factor_authentication_backup_code`, `two_factor_authentication_qr_code_uri`, `two_factor_authentication_secret`.
@@ -0,0 +1,24 @@
1
+ module RoseQuartz
2
+ class InstallGenerator < Rails::Generators::Base
3
+ include Rails::Generators::Migration
4
+
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def copy_initializer
8
+ copy_file 'initializer.rb', 'config/initializers/rose_quartz.rb'
9
+ end
10
+
11
+ def copy_locale
12
+ copy_file 'locale.en.yml', 'config/locales/rose_quartz.en.yml'
13
+ end
14
+
15
+ def copy_migration
16
+ migration_template 'migration.rb', 'db/migrate/add_two_factor_auth_with_rose_quartz.rb'
17
+ end
18
+
19
+ def self.next_migration_number(path)
20
+ next_migration_number = current_migration_number(path) + 1
21
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ RoseQuartz.initialize! do |config|
2
+ # Token issuer is used as a title in user applications (e.g. Google Authenticator).
3
+ # It is included in the QR code, and changing it won't have an effect on users
4
+ # that already have two-factor authentication enabled.
5
+ config.issuer = 'My Rails Application'
6
+
7
+ # In addition to +issuer+, client-side applications display an identifier
8
+ # (usually, this is account's email address).
9
+ # This setting needs to refer to an existing attribute or method of the authenticatable model.
10
+ config.user_identifier = :email
11
+
12
+ # Some users may have their devices slightly ahead or behind of the actual time.
13
+ # To counter this, the authenticator will accept tokens that are generated for
14
+ # timestamps withing the time drift window defined below.
15
+ config.time_drift = 2.minutes
16
+ end
@@ -0,0 +1,4 @@
1
+ en:
2
+ rose_quartz:
3
+ backup_code_used: "The backup code you have used to sign in has been reset. Please go to your account settings to copy the new one or temporarily disable two-factor authentication."
4
+ invalid_token_when_enabling_tfa: "Invalid token. Please make sure that your device has the correct time settings."
@@ -0,0 +1,14 @@
1
+ class AddTwoFactorAuthWithRoseQuartz < ActiveRecord::Migration[5.0]
2
+ def up
3
+ create_table :user_authenticators do |t|
4
+ t.integer :user_id
5
+ t.string :secret
6
+ t.string :backup_code
7
+ t.integer :last_authenticated_at
8
+ end
9
+ end
10
+
11
+ def down
12
+ drop_table :user_authenticators
13
+ end
14
+ end
@@ -1,7 +1,11 @@
1
- # frozen_string_literal: truez
1
+ # frozen_string_literal: true
2
2
  require 'devise'
3
3
 
4
4
  require 'rose_quartz/version'
5
+
6
+ require 'rose_quartz/hooks'
5
7
  require 'rose_quartz/configuration'
6
8
  require 'rose_quartz/user_authenticator'
7
9
  require 'rose_quartz/devise/strategies/two_factor_authenticatable'
10
+ require 'rose_quartz/devise/controllers/sessions_controller_extensions'
11
+ require 'rose_quartz/devise/controllers/registrations_controller_extensions'
@@ -8,23 +8,16 @@ module RoseQuartz
8
8
 
9
9
  def self.initialize!
10
10
  yield self.configuration
11
- insert_authentication_strategy!
12
- end
13
-
14
- def self.insert_authentication_strategy!
15
- ::Devise.setup do |c|
16
- c.warden do |manager|
17
- manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
18
- end
19
- end
11
+ Hooks.initialize_hooks!
20
12
  end
21
13
 
22
14
  class Configuration
23
- attr_accessor :issuer, :time_drift
15
+ attr_accessor :issuer, :time_drift, :user_identifier
24
16
 
25
17
  def initialize
26
- @issuer = ''
27
- @time_drift = 60.seconds
18
+ @issuer = 'RoseQuartz'
19
+ @time_drift = 1.minute
20
+ @user_identifier = :email
28
21
  end
29
22
  end
30
23
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ require 'i18n'
3
+ require 'rqrcode'
4
+
5
+ module Devise
6
+ module RegistrationsControllerExtensions
7
+ def self.prepended(base)
8
+ base.class_eval do
9
+ helper_method :two_factor_authentication_enabled?,
10
+ :two_factor_authentication_backup_code,
11
+ :two_factor_authentication_qr_code_uri,
12
+ :two_factor_authentication_secret
13
+ end
14
+ end
15
+
16
+ def update_resource(resource, _params)
17
+ result = super
18
+ edit_two_factor_authentication(resource) if result
19
+ result
20
+ end
21
+
22
+ protected
23
+
24
+ # View helpers
25
+
26
+ def two_factor_authentication_enabled?(user = resource)
27
+ RoseQuartz::UserAuthenticator.exists? user_id: user.id
28
+ end
29
+
30
+ def two_factor_authentication_backup_code
31
+ authenticator(resource).backup_code
32
+ end
33
+
34
+ def two_factor_authentication_qr_code_uri(size:)
35
+ uri = authenticator.provisioning_uri
36
+ qr = RQRCode::QRCode.new(uri)
37
+ qr.as_png(size: size).to_data_url
38
+ end
39
+
40
+ def two_factor_authentication_secret
41
+ authenticator.secret
42
+ end
43
+
44
+ private
45
+
46
+ # Internal logic
47
+
48
+ def edit_two_factor_authentication(resource)
49
+ if two_factor_authentication_enabled?
50
+ disable_two_factor_authentication!(resource) if form_params[:disable] == '1'
51
+ reset_two_factor_authentication_backup_code!(resource) if form_params[:reset_backup_code] == '1'
52
+ else
53
+ enable_two_factor_authentication!(resource)
54
+ end
55
+ end
56
+
57
+ def disable_two_factor_authentication!(resource)
58
+ authenticator(resource).disable!
59
+ end
60
+
61
+ def reset_two_factor_authentication_backup_code!(resource)
62
+ authenticator(resource).reset_backup_code!
63
+ end
64
+
65
+ def enable_two_factor_authentication!(resource)
66
+ secret, token = form_params.values_at(:secret, :token)
67
+ authenticator = RoseQuartz::UserAuthenticator.new(user: resource, secret: secret)
68
+ token_valid = authenticator.authenticate_otp!(token) rescue false
69
+ if token_valid
70
+ authenticator.save
71
+ else
72
+ resource.errors.add(:base, I18n.t('rose_quartz.invalid_token_when_enabling_tfa'))
73
+ end
74
+ end
75
+
76
+ def form_params
77
+ params.require(:two_factor_authentication).permit(:secret, :token, :disable, :reset_backup_code)
78
+ end
79
+
80
+ def authenticator(existing_user = nil)
81
+ @authenticator ||= if existing_user
82
+ RoseQuartz::UserAuthenticator.find_by(user_id: resource.id)
83
+ else
84
+ RoseQuartz::UserAuthenticator.new(user: resource)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devise
4
+ module SessionsControllerExtensions
5
+ def create
6
+ super do |_resource|
7
+ if request.env['rose_quartz.backup_code_used']
8
+ flash[:alert] = t('rose_quartz.backup_code_used')
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -7,17 +7,30 @@ module Devise
7
7
  def authenticate!
8
8
  resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
9
9
 
10
- super if validate(resource) { otp_matches?(resource) }
10
+ super if validate(resource) { authenticated?(resource) }
11
11
  end
12
12
 
13
- def otp_matches?(resource)
13
+ def authenticated?(resource)
14
14
  authenticator = RoseQuartz::UserAuthenticator.find_by(user_id: resource.id)
15
- return true if authenticator.nil? # two-factor authentication is disabled
15
+ token = params['otp']
16
16
 
17
- token = params['tf_authentication_token']
17
+ # Two-factor authentication is disabled
18
+ return true if authenticator.nil?
19
+
20
+ # Token is not provided
18
21
  return false if token.nil?
19
22
 
20
- authenticator.authenticate(token)
23
+ # Token is a valid OTP
24
+ return true if authenticator.authenticate_otp!(token)
25
+
26
+ # Token is a valid backup code
27
+ if authenticator.authenticate_backup_code!(token)
28
+ env['rose_quartz.backup_code_used'] = true
29
+ return true
30
+ end
31
+
32
+ # Token is not a valid OTP or backup code
33
+ false
21
34
  end
22
35
  end
23
36
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module RoseQuartz
3
+ module Hooks
4
+ def self.initialize_hooks!
5
+ ::Devise.setup do |c|
6
+ c.warden do |manager|
7
+ manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
8
+ end
9
+ end
10
+
11
+ ::Devise::SessionsController.prepend Devise::SessionsControllerExtensions
12
+ ::Devise::RegistrationsController.prepend Devise::RegistrationsControllerExtensions
13
+ end
14
+ end
15
+ end
@@ -5,22 +5,52 @@ module RoseQuartz
5
5
  class UserAuthenticator < ::ActiveRecord::Base
6
6
  belongs_to :user
7
7
 
8
- before_create :set_secret
8
+ after_initialize :set_secret_and_backup_code, if: :new_record?
9
9
 
10
- def set_secret
11
- self.secret = ROTP::Base32.random_base32
10
+ def set_secret_and_backup_code
11
+ self.secret ||= ROTP::Base32.random_base32
12
+ self.backup_code ||= generate_backup_code
12
13
  end
13
14
 
14
- def authenticator
15
- @authenticator ||= ROTP::TOTP.new(secret)
15
+ def authenticate_otp!(token)
16
+ authenticated_at = totp.verify_with_drift_and_prior(
17
+ token, RoseQuartz.configuration.time_drift, last_authenticated_at)
18
+ if authenticated_at
19
+ update_columns last_authenticated_at: authenticated_at if persisted?
20
+ true
21
+ else
22
+ false
23
+ end
16
24
  end
17
25
 
18
- def authenticate(token)
19
- authenticated_at = authenticator.verify_with_drift_and_prior(
20
- token, RoseQuartz.configuration.time_drift, last_authenticated_at)
21
- return false unless authenticated_at
22
- update_columns last_authenticated_at: authenticated_at
23
- true
26
+ def authenticate_backup_code!(token)
27
+ if token == backup_code
28
+ reset_backup_code!
29
+ true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def reset_backup_code!
36
+ update_columns backup_code: generate_backup_code
37
+ end
38
+
39
+ def totp
40
+ @authenticator ||= ROTP::TOTP.new(secret, issuer: RoseQuartz.configuration.issuer)
41
+ end
42
+
43
+ def provisioning_uri
44
+ totp.provisioning_uri(user.send(RoseQuartz.configuration.user_identifier))
45
+ end
46
+
47
+ alias disable! delete
48
+
49
+ private
50
+
51
+ # Four groups of 4-character base32 strings joined by dashes, e.g. "gs3w-ntpt-hrse-v23t"
52
+ def generate_backup_code
53
+ ROTP::Base32.random_base32(16).scan(/.{1,4}/).join('-')
24
54
  end
25
55
  end
26
56
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module RoseQuartz
3
- VERSION = '0.1.0'
3
+ VERSION = '0.9.0'
4
4
  end
@@ -16,11 +16,15 @@ Gem::Specification.new do |spec|
16
16
  end
17
17
  spec.require_paths = ['lib']
18
18
 
19
- spec.add_runtime_dependency 'rails', '~> 5.0'
20
- spec.add_runtime_dependency 'devise', '~> 4.2'
21
- spec.add_runtime_dependency 'rotp', '~> 3.3'
19
+ spec.add_runtime_dependency 'rails', '>= 5.0'
20
+ spec.add_runtime_dependency 'devise', '>= 4.2'
21
+ spec.add_runtime_dependency 'rotp', '>= 3.3'
22
+ spec.add_runtime_dependency 'rqrcode', '>= 0.10'
22
23
 
23
24
  spec.add_development_dependency 'sqlite3'
25
+ spec.add_development_dependency 'capybara'
24
26
  spec.add_development_dependency 'factory_girl_rails'
25
27
  spec.add_development_dependency 'minitest-reporters'
28
+ spec.add_development_dependency 'simplecov'
29
+ spec.add_development_dependency 'codeclimate-test-reporter'
26
30
  end
metadata CHANGED
@@ -1,57 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rose_quartz
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - little-bobby-tables
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-21 00:00:00.000000000 Z
11
+ date: 2017-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: devise
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '4.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '4.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rotp
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.3'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rqrcode
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: sqlite3
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: capybara
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'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: factory_girl_rails
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,34 @@ dependencies:
94
122
  - - ">="
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: codeclimate-test-reporter
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
97
153
  description:
98
154
  email:
99
155
  - little-bobby-tables@users.noreply.github.com
@@ -107,9 +163,16 @@ files:
107
163
  - LICENSE
108
164
  - README.md
109
165
  - Rakefile
166
+ - lib/generators/rose_quartz/install_generator.rb
167
+ - lib/generators/rose_quartz/templates/initializer.rb
168
+ - lib/generators/rose_quartz/templates/locale.en.yml
169
+ - lib/generators/rose_quartz/templates/migration.rb
110
170
  - lib/rose_quartz.rb
111
171
  - lib/rose_quartz/configuration.rb
172
+ - lib/rose_quartz/devise/controllers/registrations_controller_extensions.rb
173
+ - lib/rose_quartz/devise/controllers/sessions_controller_extensions.rb
112
174
  - lib/rose_quartz/devise/strategies/two_factor_authenticatable.rb
175
+ - lib/rose_quartz/hooks.rb
113
176
  - lib/rose_quartz/user_authenticator.rb
114
177
  - lib/rose_quartz/version.rb
115
178
  - rose_quartz.gemspec